@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
|
@@ -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,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"}
|
package/dist/bbradarClient.d.ts
CHANGED
|
@@ -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;
|