@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.
@@ -0,0 +1,106 @@
1
+ import http from "node:http";
2
+ import { performance } from "node:perf_hooks";
3
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
5
+ import { BBRadarClient } from "../dist/bbradarClient.js";
6
+ import { createBbradarServer } from "../dist/server.js";
7
+
8
+ const counters = { requests: 0, bytes: 0, active: 0, peak: 0 };
9
+ const historyRows = Array.from({ length: 10 }, (_, i) => ({ id: `c${i}`, program: { id: "HackerOne:bench" }, category: "scope", observed_at: "2026-09-24T00:00:00Z" }));
10
+ const responses = {
11
+ "/pro/history/changes": { changes: historyRows, coverage: { capture_enabled: true }, meta: { has_more: false, next_cursor: null } },
12
+ "/pro/activity/programs/HackerOne:bench": { program_id: "HackerOne:bench", activity_summary: { additions: 0, last_observed_at: null } },
13
+ "/pro/analytics/programs/activity": { programs: historyRows.map((row) => ({ id: row.program.id, activity_days: 1 })), meta: { generation: 3, provisional: false, unsupported_metrics: ["repository_changes"] } }
14
+ };
15
+ // Activity catalog: every fifth program passes the Web2, wildcard, competition
16
+ // and fee filters of the combined benchmark question.
17
+ const catalog = Array.from({ length: 250 }, (_, i) => ({ id: `HackerOne:p${i}`, platform: "HackerOne", handle: `p${i}`, name: `Program ${i}`, program_type: "paid", is_active: true,
18
+ bounty_min: 100, bounty_max: 5000, report_count: 10, competition_risk: i % 5 === 3 ? "High" : "Low",
19
+ scope_tags: i % 5 === 1 ? ["smart-contract"] : ["domain", "api", "wildcard"], submission_cost: i % 5 === 2 ? { amount: 25, maximum_amount: 25 } : null,
20
+ target_counts: { total: 30, in_scope: 30, eligible_for_bounty: 30, wildcard: i % 5 === 4 ? 0 : 3 }, payout_summary: { "30d": { amount: "1500", currency: "USD" } } }));
21
+ const catalogEndpoint = { enabled: true, max_page_size: 100, activity_filters: { enabled: true }, catalog_filters: { enabled: true } };
22
+ function catalogPage(url) {
23
+ const page = Number(url.searchParams.get("page") ?? 1), size = Number(url.searchParams.get("page_size") ?? 50);
24
+ return { programs: catalog.slice((page - 1) * size, page * size), meta: { page, page_size: size, total: catalog.length, total_pages: Math.ceil(catalog.length / size), activity_snapshot_id: "a".repeat(64) } };
25
+ }
26
+ const api = http.createServer(async (req, res) => {
27
+ const url = new URL(req.url, "http://fixture");
28
+ const path = url.pathname.replace(/^\/api\/v1/, "");
29
+ counters.requests += 1;
30
+ counters.active += 1;
31
+ counters.peak = Math.max(counters.peak, counters.active);
32
+ await new Promise((resolve) => setTimeout(resolve, 5));
33
+ const payload = path === "/pro/capabilities"
34
+ ? { contract_version: "2026-10-06", endpoints: { ...Object.fromEntries(["history_changes", "history_change_detail", "program_activity", "analytics_program_activity", "activity_rankings"].map((key) => [key, { enabled: true, history_capture_enabled: true }])), programs: catalogEndpoint, opportunities: catalogEndpoint, program_targets: { max_page_size: 500 } } }
35
+ : path === "/pro/programs" ? catalogPage(url)
36
+ : /^\/pro\/programs\/.+\/targets$/.test(path) ? { targets: [{ identifier: "api.example.test", target_type: "domain", in_scope: true, eligible_for_bounty: true }], meta: { total: 1, total_pages: 1 } }
37
+ : path === "/pro/activity/rankings" ? { tab: "payouts", period: "30d", sort: "paid", direction: "desc", rows: catalog.slice(0, 10).map((program) => ({ program, paid: program.payout_summary["30d"] })), capabilities: { tabs: [{ id: "payouts", views: [] }], facets: { tags: Array.from({ length: 300 }, (_, i) => `tag-${i}`) } }, meta: { total: 250, has_more: true, next_cursor: "next", snapshot_id: "a".repeat(64) } }
38
+ : path.startsWith("/pro/history/changes/")
39
+ ? { change: historyRows[0], items: [{ before: null, after: { count: 0 } }], meta: { has_more: false } }
40
+ : responses[path] ?? { detail: "missing" };
41
+ const body = JSON.stringify(payload);
42
+ counters.bytes += Buffer.byteLength(body);
43
+ res.setHeader("content-type", "application/json");
44
+ res.setHeader("cache-control", "private, no-store");
45
+ res.end(body);
46
+ counters.active -= 1;
47
+ });
48
+
49
+ await new Promise((resolve) => api.listen(0, "127.0.0.1", resolve));
50
+ const base = `http://127.0.0.1:${api.address().port}/api/v1`;
51
+ const options = { apiKey: "benchmark", apiBaseUrl: base, webBaseUrl: "https://bbradar.io", cacheTtlMs: 30_000, negativeCacheTtlMs: 15_000, cacheMaxEntries: 50, defaultRateLimitPerMinute: 60, exportRateLimitPerMinute: 15, maxConcurrentRequests: 6, compactToolText: true, toolProfile: "core", requestTimeoutMs: 5_000, validateOnStartup: false };
52
+ const upstream = new BBRadarClient(options);
53
+ await upstream.negotiateCapabilities();
54
+ const server = createBbradarServer(upstream, options);
55
+ const client = new Client({ name: "pro-benchmark", version: "1" });
56
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
57
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
58
+
59
+ function reset() { counters.requests = 0; counters.bytes = 0; counters.peak = 0; }
60
+ function percentile(values, p) { const sorted = [...values].sort((a, b) => a - b); return Math.round(sorted[Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1)] * 100) / 100; }
61
+ async function measure(name, args) {
62
+ reset();
63
+ const timings = [];
64
+ let result;
65
+ const heapBefore = process.memoryUsage().heapUsed;
66
+ for (let i = 0; i < 20; i += 1) {
67
+ const start = performance.now();
68
+ result = await client.callTool({ name, arguments: args });
69
+ timings.push(performance.now() - start);
70
+ if (result.isError) throw new Error(`${name} failed`);
71
+ }
72
+ return { calls: 20, upstream_requests: counters.requests, upstream_bytes: counters.bytes, model_text_bytes: Buffer.byteLength(result.content[0].text), structured_bytes: Buffer.byteLength(JSON.stringify(result.structuredContent)), p50_ms: percentile(timings, .5), p95_ms: percentile(timings, .95), heap_delta_bytes: process.memoryUsage().heapUsed - heapBefore };
73
+ }
74
+
75
+ async function measureDirect(path) {
76
+ reset();
77
+ const timings = [];
78
+ for (let i = 0; i < 20; i += 1) {
79
+ const start = performance.now();
80
+ const response = await fetch(base + path, { headers: { authorization: "Bearer benchmark" } });
81
+ await response.arrayBuffer();
82
+ timings.push(performance.now() - start);
83
+ }
84
+ return { calls: 20, upstream_requests: counters.requests, upstream_bytes: counters.bytes, p50_ms: percentile(timings, .5), p95_ms: percentile(timings, .95) };
85
+ }
86
+
87
+ try {
88
+ const directHistory = await measureDirect("/pro/history/changes?limit=10");
89
+ const history = await measure("get_history_changes", { limit: 10 });
90
+ const detail = await measure("get_history_change_detail", { change_id: "c0", limit: 1 });
91
+ const activity = await measure("get_program_activity", { program_id: "HackerOne:bench" });
92
+ const analytics = await measure("get_analytics_program_activity", { limit: 10 });
93
+ const activityRanking = await measure("get_activity_rankings", { tab: "payouts", period: "30d", limit: 10 });
94
+ // "Recent payouts, low competition, new scope, wide Web2 scope, no submission fee" in one call.
95
+ const question = { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, exclude_web3: true, min_wildcards: 1, competition_risks: ["Low"], free_submission_only: true };
96
+ const combinedQuestion = await measure("find_programs", { ...question, max_results: 10 });
97
+ const combinedHunt = await measure("find_hunt_targets", { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", min_wildcards: 1, free_submission_only: true, include_recent_changes: false, max_results: 5 });
98
+ reset();
99
+ const heapBefore = process.memoryUsage().heapUsed;
100
+ const parallel = await Promise.all(Array.from({ length: 10 }, (_, i) => client.callTool({ name: "get_history_changes", arguments: { search: `bench${i}`, limit: 1 } })));
101
+ const concurrency = { calls: parallel.length, upstream_requests: counters.requests, peak_upstream_requests: counters.peak, heap_delta_bytes: process.memoryUsage().heapUsed - heapBefore };
102
+ if ([history, detail, activity, analytics, activityRanking, combinedQuestion].some((entry) => entry.upstream_requests !== 20) || concurrency.peak_upstream_requests > 2 || concurrency.upstream_requests !== 10) throw new Error("Pro request-count or shared-concurrency regression");
103
+ // The candidate pool's catalog pages plus one exact target sample per finalist.
104
+ if (combinedHunt.upstream_requests > 20 * (3 + 5)) throw new Error(`Activity hunt request-count regression: ${combinedHunt.upstream_requests}`);
105
+ process.stdout.write(JSON.stringify({ fixture: true, simulated_upstream_delay_ms: 5, direct_history: directHistory, estimated_mcp_overhead_p50_ms: Math.round((history.p50_ms - directHistory.p50_ms) * 100) / 100, history_lazy_detail: { eager_baseline_requests: 11, feed_only_requests: 1, requested_detail_requests: 2 }, tools: { history, detail, activity, analytics, activity_ranking: activityRanking, combined_question: combinedQuestion, combined_hunt: combinedHunt }, concurrency }, null, 2) + "\n");
106
+ } finally { await client.close(); await server.close(); api.close(); }
@@ -0,0 +1,2 @@
1
+ /** Catalog metadata stays available when Activity computation is disabled. */
2
+ export declare function requiresActivityQuery(input?: Record<string, unknown>): boolean;
@@ -0,0 +1,15 @@
1
+ const ACTIVITY_FILTERS = new Set([
2
+ "payout_metric", "payout_period", "payout_currency", "payout_precision", "payout_min", "payout_max",
3
+ "payout_positive", "payout_activity", "scope_period", "scope_added_min", "scope_removed_min",
4
+ "scope_net_min", "scope_net_max", "reward_period", "reward_change", "reward_change_min_pct",
5
+ "updates_period", "active_days_min", "changes_min", "change_category", "reports_period",
6
+ "reports_min", "reports_max", "reports_change", "reports_change_min_pct", "launched_within",
7
+ "resolved_period", "resolved_min", "last_resolved_within", "first_response_max_hours",
8
+ "time_to_bounty_max_hours", "status_period", "status_change", "current_status", "activity_labels",
9
+ ]);
10
+ /** Catalog metadata stays available when Activity computation is disabled. */
11
+ export function requiresActivityQuery(input = {}) {
12
+ return Object.entries(input).some(([key, value]) => value !== undefined && value !== null
13
+ && (key === "sort_by" ? value !== "updated" : ACTIVITY_FILTERS.has(key)));
14
+ }
15
+ //# sourceMappingURL=activityContract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activityContract.js","sourceRoot":"","sources":["../src/activityContract.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,YAAY;IACnG,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,mBAAmB;IAC5F,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB;IAC3F,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB;IACvF,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,iBAAiB;IAC3F,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,0BAA0B;IACrF,0BAA0B,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB;CAClG,CAAC,CAAC;AAEH,8EAA8E;AAC9E,MAAM,UAAU,qBAAqB,CAAC,QAAiC,EAAE;IACvE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;WACpF,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC"}
@@ -9,8 +9,9 @@ export type ApiRequestOptions = {
9
9
  coalesce?: boolean;
10
10
  staleWhileRevalidateMs?: number;
11
11
  timeoutMs?: number;
12
+ signal?: AbortSignal;
12
13
  };
13
- export type ApiReadControls = Pick<ApiRequestOptions, "cache" | "coalesce" | "timeoutMs">;
14
+ export type ApiReadControls = Pick<ApiRequestOptions, "cache" | "coalesce" | "timeoutMs" | "signal">;
14
15
  export type ApiResult<T> = {
15
16
  data: T;
16
17
  requestId: string;
@@ -21,6 +22,7 @@ export type ApiResult<T> = {
21
22
  fetchedAt: string;
22
23
  cacheExpiresAt?: string | undefined;
23
24
  stale?: boolean | undefined;
25
+ noStore?: boolean | undefined;
24
26
  };
25
27
  type CapabilityNegotiationState = {
26
28
  status: "not_started" | "negotiated" | "legacy" | "failed";
@@ -37,6 +39,7 @@ export declare class BBRadarApiError extends Error {
37
39
  readonly cached: boolean;
38
40
  readonly coalesced: boolean;
39
41
  readonly cacheExpiresAt: string | undefined;
42
+ readonly cacheAllowed: boolean;
40
43
  constructor(params: {
41
44
  message: string;
42
45
  requestId: string;
@@ -47,6 +50,7 @@ export declare class BBRadarApiError extends Error {
47
50
  cached?: boolean | undefined;
48
51
  coalesced?: boolean | undefined;
49
52
  cacheExpiresAt?: string | undefined;
53
+ cacheAllowed?: boolean | undefined;
50
54
  cause?: unknown;
51
55
  });
52
56
  }
@@ -55,6 +59,8 @@ export declare class BBRadarClient {
55
59
  private readonly cache;
56
60
  private readonly negativeCache;
57
61
  private readonly inFlight;
62
+ private readonly sharedRequests;
63
+ private readonly requestContext;
58
64
  private readonly endpointMetrics;
59
65
  private readonly requestQueue;
60
66
  private readonly endpointRequestQueues;
@@ -67,6 +73,7 @@ export declare class BBRadarClient {
67
73
  private capabilityNegotiation;
68
74
  private capabilityState;
69
75
  constructor(config: ServerConfig);
76
+ withRequestContext<T>(signal: AbortSignal, callback: () => T | Promise<T>, snapshotStart?: boolean): Promise<T>;
70
77
  diagnostics(): {
71
78
  response_cache: {
72
79
  entries: number;
@@ -192,8 +199,21 @@ export declare class BBRadarClient {
192
199
  getRepositoryChanges(query: Record<string, QueryValue>, controls?: ApiReadControls): Promise<ApiResult<unknown>>;
193
200
  searchTargets(query: Record<string, QueryValue>, controls?: ApiReadControls): Promise<ApiResult<unknown>>;
194
201
  getOpportunities(level: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
202
+ getHistoryChanges(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
203
+ getHistoryChangeDetail(changeId: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
204
+ getProgramHistory(programId: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
205
+ getProgramActivity(programId: string): Promise<ApiResult<unknown>>;
206
+ getActivityCapabilities(query?: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
207
+ getActivitySnapshot(): Promise<ApiResult<unknown>>;
208
+ getActivityRankings(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
209
+ getActivityChangeDetail(eventId: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
210
+ getProgramPayouts(programId: string, query?: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
211
+ getAnalyticsPlatformSeries(platform: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
212
+ getAnalyticsProgramActivity(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
213
+ getAnalyticsProgramDetail(programId: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
195
214
  exportTargets(body: Record<string, unknown>): Promise<ApiResult<unknown>>;
196
215
  request<T = unknown>(options: ApiRequestOptions): Promise<ApiResult<T>>;
216
+ private requestWithSignal;
197
217
  private fetchFromApi;
198
218
  private refreshInBackground;
199
219
  private withRequestSlot;