@certscore/mcp 0.2.12 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,10 +1,64 @@
1
- import { CertScoreClient, CertScoreTimeoutError } from "@certscore/sdk";
2
- import { certScoreMcpToolContracts } from "@certscore/api-contracts";
1
+ import { CertScoreClient } from "@certscore/sdk";
2
+ import { certScoreMcpToolContracts, isCanonicalScanId } from "@certscore/api-contracts";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { CERTSCORE_MCP_VERSION } from "./version.js";
5
- import { boundEvidencePacket, buildScanBundle, exportFindings, limitPreConsentRows, normalizeDetail, normalizeFormat, paginateFindingList, scanIdFromStatus, toToolError, toToolResult } from "./tools.js";
6
- let createScanDeprecationWarningPrinted = false;
7
- const DEFAULT_MCP_SCAN_WAIT_MS = 45_000;
5
+ import { boundEvidencePacket, buildScanBundle, exportFindings, findingListText, limitPreConsentRows, markdownReportText, MAX_EVIDENCE_PACKET_CHARS, normalizeDetail, normalizeFormat, paginateFindingList, preConsentInventoryText, pulseReportText, scanBundleText, scanStatusText, toInvalidArgumentsToolError, toInvalidScanIdToolError, toToolError, toToolResult, withMcpAgentGuidance, withMcpScanProvenanceGuidance } from "./tools.js";
6
+ const DEFAULT_MCP_SCAN_TOOL_BUDGET_MS = 25_000;
7
+ const MAX_MCP_SCAN_TOOL_BUDGET_MS = 45_000;
8
+ const MCP_SCAN_TOOL_RESPONSE_RESERVE_MS = 1_000;
9
+ const LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES = 25_000;
10
+ export function resolveMcpScanSiteWaitBudget(input) {
11
+ const totalBudgetMs = Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_TOOL_BUDGET_MS, MAX_MCP_SCAN_TOOL_BUDGET_MS);
12
+ const elapsedMs = Math.max(0, input.nowMs - input.startedAtMs);
13
+ return {
14
+ elapsedMs,
15
+ remainingWaitMs: Math.max(0, totalBudgetMs - elapsedMs - MCP_SCAN_TOOL_RESPONSE_RESERVE_MS),
16
+ responseReserveMs: MCP_SCAN_TOOL_RESPONSE_RESERVE_MS,
17
+ totalBudgetMs,
18
+ };
19
+ }
20
+ function exampleDomainDemoSubstitution(requestedUrl, demoUrl) {
21
+ if (!demoUrl)
22
+ return null;
23
+ try {
24
+ const parsed = new URL(requestedUrl.includes("://") ? requestedUrl : `https://${requestedUrl}`);
25
+ const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
26
+ const reserved = ["example.com", "example.net", "example.org"].some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
27
+ if (!reserved)
28
+ return null;
29
+ return {
30
+ requestedUrl,
31
+ effectiveUrl: demoUrl,
32
+ reason: "iana_example_domain",
33
+ message: "The requested IANA example domain is a documentation placeholder, so CertScore scanned its controlled demonstration site instead. Findings describe the effective URL, not the requested placeholder."
34
+ };
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function withExampleDomainDemo(value, substitution) {
41
+ return substitution ? { ...value, demoSubstitution: substitution } : value;
42
+ }
43
+ function exampleDomainDemoText(value, substitution) {
44
+ if (!substitution)
45
+ return undefined;
46
+ const status = typeof value.status === "string" ? ` Status=${value.status}.` : "";
47
+ const scanId = typeof value.scanId === "string" ? ` ScanId=${value.scanId}.` : "";
48
+ return `${substitution.message}${status}${scanId} Full result and substitution provenance are in structuredContent.`;
49
+ }
50
+ async function retryTransientOriginFailure(operation) {
51
+ try {
52
+ return await operation();
53
+ }
54
+ catch (error) {
55
+ const retryable = error instanceof Error && "status" in error && [502, 503, 504].includes(Number(error.status));
56
+ if (!retryable) {
57
+ throw error;
58
+ }
59
+ return operation();
60
+ }
61
+ }
8
62
  function scanCreationMetadata(value) {
9
63
  return {
10
64
  executionMode: value.executionMode,
@@ -32,99 +86,291 @@ function toolContract(name) {
32
86
  annotations: contract.annotations
33
87
  };
34
88
  }
89
+ function boundedTelemetryToken(value, maxLength) {
90
+ return typeof value === "string" && /^[a-zA-Z0-9_.:-]+$/.test(value) && value.length <= maxLength
91
+ ? value
92
+ : null;
93
+ }
94
+ function telemetryResultRecord(result) {
95
+ if (!result || typeof result !== "object" || Array.isArray(result))
96
+ return {};
97
+ const toolResult = result;
98
+ if (toolResult.structuredContent && typeof toolResult.structuredContent === "object" && !Array.isArray(toolResult.structuredContent)) {
99
+ return toolResult.structuredContent;
100
+ }
101
+ const firstText = Array.isArray(toolResult.content)
102
+ ? toolResult.content.find((item) => item && typeof item === "object" && item.type === "text")
103
+ : null;
104
+ if (!firstText || typeof firstText.text !== "string")
105
+ return {};
106
+ try {
107
+ const parsed = JSON.parse(firstText.text);
108
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
109
+ }
110
+ catch {
111
+ return {};
112
+ }
113
+ }
114
+ function telemetryHostname(value) {
115
+ if (typeof value !== "string")
116
+ return null;
117
+ try {
118
+ const parsed = new URL(value.includes("://") ? value : `https://${value}`);
119
+ return parsed.hostname.toLowerCase().replace(/\.$/, "").slice(0, 253) || null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ function telemetryUrl(value) {
126
+ if (typeof value !== "string")
127
+ return null;
128
+ try {
129
+ const parsed = new URL(value.includes("://") ? value : `https://${value}`);
130
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
131
+ return null;
132
+ return parsed.origin.slice(0, 512);
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ function requestedTelemetryResource(args) {
139
+ const scanId = boundedTelemetryToken(args.scanId, 128);
140
+ if (scanId)
141
+ return { requestedResource: scanId, requestedResourceType: "scan_id" };
142
+ const jobId = boundedTelemetryToken(args.jobId, 128);
143
+ if (jobId)
144
+ return { requestedResource: jobId, requestedResourceType: "job_id" };
145
+ const url = telemetryUrl(args.url);
146
+ if (url)
147
+ return { requestedResource: url, requestedResourceType: "url" };
148
+ const domain = telemetryHostname(args.domain);
149
+ if (domain)
150
+ return { requestedResource: domain, requestedResourceType: "domain" };
151
+ return { requestedResource: null, requestedResourceType: null };
152
+ }
153
+ function isCertScoreCanaryUrl(value) {
154
+ if (typeof value !== "string")
155
+ return false;
156
+ try {
157
+ const parsed = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
158
+ return parsed.pathname.startsWith("/.well-known/certscore-canary/");
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ export function projectMcpToolInvocationObservation(input) {
165
+ const args = input.args && typeof input.args === "object" && !Array.isArray(input.args)
166
+ ? input.args
167
+ : {};
168
+ const result = telemetryResultRecord(input.result);
169
+ const error = result.error && typeof result.error === "object" && !Array.isArray(result.error)
170
+ ? result.error
171
+ : null;
172
+ const errorCode = boundedTelemetryToken(error?.code ?? result.errorCode, 100);
173
+ const rateLimited = errorCode === "rate_limited" || result.status === "rate_limited";
174
+ const isError = Boolean(input.result?.isError) || Boolean(error);
175
+ const outcome = rateLimited ? "rate_limited" : isError ? "error" : "success";
176
+ const resultScanId = boundedTelemetryToken(result.scanId ?? result.scan_id ?? result.jobId, 128);
177
+ const inputScanId = boundedTelemetryToken(args.scanId, 128);
178
+ const requestedResource = requestedTelemetryResource(args);
179
+ const targetHostname = input.toolName === "certscore_scan_site"
180
+ ? telemetryHostname(args.url)
181
+ : input.toolName === "certscore_get_latest_domain_scan"
182
+ || input.toolName === "certscore_get_latest_domain_pre_consent_cookies_trackers"
183
+ ? telemetryHostname(args.domain)
184
+ : null;
185
+ const isCanary = isCertScoreCanaryUrl(args.url);
186
+ const executionMode = result.executionMode;
187
+ const scanDecision = input.toolName !== "certscore_scan_site"
188
+ ? "not_applicable"
189
+ : outcome !== "success"
190
+ ? "unavailable"
191
+ : result.reused === true || executionMode === "reused_scan"
192
+ ? "reused"
193
+ : result.reused === false || executionMode === "new_scan" || result.quotaConsumed === true
194
+ ? "new"
195
+ : "unavailable";
196
+ return {
197
+ durationMs: Math.max(0, Math.min(Math.round(input.durationMs), 3_600_000)),
198
+ errorCode: rateLimited ? "rate_limited" : errorCode,
199
+ freshness: args.freshness === "refresh" ? "refresh" : input.toolName === "certscore_scan_site" ? "latest" : null,
200
+ isCanary,
201
+ outcome,
202
+ quotaOutcome: rateLimited ? "rate_limited" : "allowed",
203
+ ...requestedResource,
204
+ scanDecision,
205
+ scanFrom: args.scanFrom === "eu_de" || args.scanFrom === "eu_ie" || args.scanFrom === "california"
206
+ ? args.scanFrom
207
+ : result.scanFrom === "eu_de" || result.scanFrom === "eu_ie" || result.scanFrom === "california"
208
+ ? result.scanFrom
209
+ : null,
210
+ scanId: resultScanId ?? inputScanId,
211
+ scanStatus: boundedTelemetryToken(result.status, 64),
212
+ targetHostname,
213
+ toolName: input.toolName,
214
+ transportOutcome: isError ? "mcp_error" : "mcp_result",
215
+ };
216
+ }
217
+ function observeToolInvocation(observer, observation, requestContext) {
218
+ if (!observer)
219
+ return;
220
+ queueMicrotask(() => {
221
+ Promise.resolve().then(() => observer(observation, requestContext)).catch((error) => {
222
+ console.error("[certscore-mcp] telemetry observer failed", {
223
+ errorName: error instanceof Error ? error.name : "UnknownError",
224
+ toolName: observation.toolName,
225
+ });
226
+ });
227
+ });
228
+ }
35
229
  export function createCertScoreMcpServer(options = {}) {
36
- const client = new CertScoreClient({
230
+ const createClient = (forwardedClientIp, anonymousRequesterSession) => new CertScoreClient({
37
231
  apiKey: options.apiKey,
38
232
  baseUrl: options.baseUrl,
39
233
  clientName: "mcp",
40
- forwardedClientIp: options.forwardedClientIp,
234
+ forwardedClientIp,
41
235
  anonymousRequesterSecret: options.anonymousRequesterSecret,
236
+ anonymousSurface: options.anonymousSurface,
237
+ anonymousRequesterSession,
42
238
  timeout: options.timeout
43
239
  });
240
+ const client = createClient(options.forwardedClientIp, options.resolveAnonymousRequesterSession?.());
241
+ const clientForRequest = (extra) => options.resolveForwardedClientIp
242
+ ? createClient(options.resolveForwardedClientIp(extra.requestInfo?.headers ?? {}), options.resolveAnonymousRequesterSession?.())
243
+ : client;
44
244
  const server = new McpServer({
45
245
  name: "certscore",
46
246
  version: CERTSCORE_MCP_VERSION
47
247
  });
48
- const lightTools = new Set(["scan_site", "get_scan_status", "get_scan_bundle"]);
248
+ const sdkCreateToolError = server.createToolError.bind(server);
249
+ server.createToolError = (message) => message.includes("Input validation error:")
250
+ ? toInvalidArgumentsToolError(message)
251
+ : sdkCreateToolError(message);
252
+ const lightTools = new Set(["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle"]);
253
+ const scanIdTools = new Set([
254
+ "certscore_explain_finding",
255
+ "certscore_export_findings",
256
+ "certscore_get_evidence",
257
+ "certscore_get_pre_consent_cookies_trackers",
258
+ "certscore_get_report",
259
+ "certscore_get_scan",
260
+ "certscore_get_scan_bundle",
261
+ "certscore_get_scan_status",
262
+ "certscore_list_findings",
263
+ ]);
49
264
  const registerMcpTool = server.registerTool.bind(server);
50
265
  const registerTool = (name, contract, handler) => {
51
266
  if (options.toolProfile === "light" && !lightTools.has(name)) {
52
267
  return;
53
268
  }
54
- registerMcpTool(name, contract, handler);
55
- };
56
- async function createPulseScanTool(input) {
57
- const result = await client.submitScan(input.url, {
58
- detail: normalizeDetail(input.detail),
59
- format: normalizeFormat(input.format),
60
- freshness: input.freshness ?? "latest",
61
- scanFrom: input.scanFrom
62
- });
63
- return {
64
- type: "certscore_mcp_scan_created",
65
- status: result.status,
66
- jobId: result.jobId ?? null,
67
- scanId: result.scanId ?? result.scan_id ?? null,
68
- completed: result.completed ?? false,
69
- statusUrl: result.statusUrl ?? result.nextCheckUrl ?? null,
70
- resultUrl: result.resultUrl ?? null,
71
- reportUrl: result.reportUrl ?? null,
72
- pulse: result.pulse ?? null,
73
- resultDisposition: result.resultDisposition ?? result.pulse?.resultDisposition ?? null,
74
- noGo: result.noGo ?? result.pulse?.noGo ?? null
75
- };
76
- }
77
- registerTool("create_scan", toolContract("create_scan"), async (input) => {
78
- try {
79
- if (!createScanDeprecationWarningPrinted) {
80
- createScanDeprecationWarningPrinted = true;
81
- console.error("[certscore-mcp] create_scan is deprecated in the 0.2.x line. Use scan_site for new integrations.");
269
+ const typedHandler = handler;
270
+ registerMcpTool(name, contract, async (input, extra) => {
271
+ const startedAt = Date.now();
272
+ try {
273
+ const scanId = input && typeof input === "object" && !Array.isArray(input)
274
+ ? input.scanId
275
+ : null;
276
+ const result = scanIdTools.has(name) && !isCanonicalScanId(scanId)
277
+ ? toInvalidScanIdToolError()
278
+ : await typedHandler(input, extra);
279
+ observeToolInvocation(options.onToolInvocation, projectMcpToolInvocationObservation({
280
+ args: input,
281
+ durationMs: Date.now() - startedAt,
282
+ result,
283
+ toolName: name,
284
+ }), { headers: extra.requestInfo?.headers ?? null });
285
+ return result;
82
286
  }
83
- return toToolResult(await createPulseScanTool(input));
84
- }
85
- catch (error) {
86
- return toToolError(error);
87
- }
88
- });
89
- registerTool("scan_site", toolContract("scan_site"), async (input) => {
287
+ catch (error) {
288
+ observeToolInvocation(options.onToolInvocation, {
289
+ ...projectMcpToolInvocationObservation({
290
+ args: input,
291
+ durationMs: Date.now() - startedAt,
292
+ result: { isError: true, structuredContent: { error: { code: "handler_exception" } } },
293
+ toolName: name,
294
+ }),
295
+ errorCode: "handler_exception",
296
+ outcome: "error",
297
+ transportOutcome: "mcp_error",
298
+ }, { headers: extra.requestInfo?.headers ?? null });
299
+ throw error;
300
+ }
301
+ });
302
+ };
303
+ registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input, extra) => {
304
+ const toolStartedAtMs = Date.now();
305
+ const client = clientForRequest(extra);
306
+ const demoSubstitution = exampleDomainDemoSubstitution(input.url, options.exampleDomainDemoUrl);
307
+ const effectiveUrl = demoSubstitution?.effectiveUrl ?? input.url;
90
308
  try {
91
- const created = await client.scans.create(input.url, {
309
+ const created = await client.scans.create(effectiveUrl, {
92
310
  freshness: input.freshness ?? "latest",
93
311
  scanFrom: input.scanFrom
94
312
  });
95
313
  if (input.waitForCompletion === false || created.type === "certscore_scan") {
96
- return toToolResult(created);
314
+ const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
315
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
97
316
  }
317
+ const waitBudget = resolveMcpScanSiteWaitBudget({
318
+ maxWaitSeconds: input.maxWaitSeconds,
319
+ nowMs: Date.now(),
320
+ startedAtMs: toolStartedAtMs,
321
+ });
322
+ if (waitBudget.remainingWaitMs === 0) {
323
+ console.warn(JSON.stringify({
324
+ event: "mcp.certscore_scan_site.wait_budget_consumed",
325
+ jobId: created.jobId ?? null,
326
+ scanId: created.scanId ?? created.scan_id ?? null,
327
+ status: created.status ?? null,
328
+ ...waitBudget,
329
+ }));
330
+ const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
331
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
332
+ }
333
+ const waitAbortController = new AbortController();
334
+ const waitAbortTimer = setTimeout(() => waitAbortController.abort(), waitBudget.remainingWaitMs);
98
335
  try {
336
+ const internalMcpOperation = { operation: "scan_site_wait", scanId: created.scanId ?? created.scan_id ?? created.jobId };
99
337
  const completed = await client.scans.wait(created, {
100
- maxWaitMs: Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_WAIT_MS, DEFAULT_MCP_SCAN_WAIT_MS)
338
+ maxWaitMs: waitBudget.remainingWaitMs,
339
+ internalMcpOperation,
340
+ signal: waitAbortController.signal,
101
341
  });
102
- return toToolResult({
342
+ const guided = withExampleDomainDemo(withMcpAgentGuidance({
103
343
  ...completed,
104
- ...scanCreationMetadata(created),
105
- recommendedNextTool: "get_scan_bundle"
106
- });
344
+ ...scanCreationMetadata(created)
345
+ }), demoSubstitution);
346
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
107
347
  }
108
348
  catch (error) {
109
- if (!(error instanceof CertScoreTimeoutError)) {
110
- throw error;
111
- }
112
349
  const scanId = created.scanId ?? created.scan_id;
113
- if (scanId) {
114
- return toToolResult({
115
- ...(await client.scans.status(scanId)),
116
- ...scanCreationMetadata(created),
117
- recommendedNextTool: "get_scan_status"
118
- });
119
- }
120
- return toToolResult(created);
350
+ console.warn(JSON.stringify({
351
+ event: "mcp.certscore_scan_site.wait_deferred",
352
+ errorName: error instanceof Error ? error.name : "UnknownError",
353
+ jobId: created.jobId ?? null,
354
+ scanId: scanId ?? null,
355
+ status: created.status ?? null,
356
+ ...waitBudget,
357
+ }));
358
+ // Waiting is a convenience layered on top of scan creation. Once the
359
+ // API has accepted a scan, never turn a transient polling or hydration
360
+ // failure into an identity-less tool error that encourages callers to
361
+ // submit a second, non-idempotent certscore_scan_site request.
362
+ const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
363
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
364
+ }
365
+ finally {
366
+ clearTimeout(waitAbortTimer);
121
367
  }
122
368
  }
123
369
  catch (error) {
124
370
  return toToolError(error);
125
371
  }
126
372
  });
127
- registerTool("get_scan", toolContract("get_scan"), async ({ scanId }) => {
373
+ registerTool("certscore_get_scan", toolContract("certscore_get_scan"), async ({ scanId }) => {
128
374
  try {
129
375
  return toToolResult(await client.scans.get(scanId));
130
376
  }
@@ -132,53 +378,23 @@ export function createCertScoreMcpServer(options = {}) {
132
378
  return toToolError(error);
133
379
  }
134
380
  });
135
- registerTool("get_scan_status", toolContract("get_scan_status"), async ({ jobId, scanId }) => {
381
+ registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }, extra) => {
382
+ const client = clientForRequest(extra);
136
383
  try {
137
- if (scanId) {
138
- const status = await client.scans.status(scanId);
139
- const needsTerminalHydration = status.status === "completed" || status.status === "completed_limited";
140
- if (needsTerminalHydration) {
141
- try {
142
- const scan = await client.scans.get(scanId);
143
- return toToolResult({
144
- ...status,
145
- type: "certscore_scan_job",
146
- status: scan.status,
147
- domain: scan.domain,
148
- resultDisposition: scan.resultDisposition,
149
- noGo: scan.noGo,
150
- startedAt: scan.startedAt,
151
- completedAt: scan.completedAt,
152
- scanTimeSeconds: scan.scanTimeSeconds,
153
- recommendedNextTool: "get_scan_bundle"
154
- });
155
- }
156
- catch {
157
- // Preserve the API status response if the terminal scan resource is
158
- // briefly unavailable during eventual-consistency windows.
159
- }
160
- }
161
- return toToolResult(status);
162
- }
163
- if (!jobId) {
164
- return toToolResult({
165
- error: {
166
- name: "InvalidToolInput",
167
- message: "Provide either scanId for API v2 scan status or jobId for Pulse job status."
168
- }
169
- });
170
- }
171
- const status = await client.getJobStatus(jobId);
172
- return toToolResult({
384
+ const internalMcpOperation = { operation: "scan_status", scanId };
385
+ const status = await client.scans.status(scanId, { internalMcpOperation });
386
+ const guided = withMcpScanProvenanceGuidance({
173
387
  ...status,
174
- scanId: scanIdFromStatus(status)
175
- });
388
+ jobId: undefined,
389
+ scanFrom: status.scanFrom ?? null
390
+ }, "existing_scan_retrieved");
391
+ return toToolResult(guided, scanStatusText(guided));
176
392
  }
177
393
  catch (error) {
178
394
  return toToolError(error);
179
395
  }
180
396
  });
181
- registerTool("get_report", toolContract("get_report"), async ({ scanId, detail, format }) => {
397
+ registerTool("certscore_get_report", toolContract("certscore_get_report"), async ({ scanId, detail, format }) => {
182
398
  try {
183
399
  const normalizedFormat = normalizeFormat(format);
184
400
  const result = normalizedFormat === "markdown"
@@ -190,69 +406,116 @@ export function createCertScoreMcpServer(options = {}) {
190
406
  detail: normalizeDetail(detail),
191
407
  format: "json"
192
408
  });
193
- return toToolResult(result);
409
+ if (typeof result === "string") {
410
+ const guided = withMcpAgentGuidance({
411
+ type: "certscore_pulse_markdown",
412
+ scanId,
413
+ value: result
414
+ }, "existing_scan_retrieved");
415
+ return toToolResult(guided, markdownReportText(guided));
416
+ }
417
+ const guided = withMcpAgentGuidance(result, "existing_scan_retrieved");
418
+ return toToolResult(guided, pulseReportText(guided));
194
419
  }
195
420
  catch (error) {
196
421
  return toToolError(error);
197
422
  }
198
423
  });
199
- registerTool("get_evidence", toolContract("get_evidence"), async ({ scanId }) => {
424
+ registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }) => {
200
425
  try {
201
- return toToolResult(boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" })));
426
+ const bounded = boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" }), MAX_EVIDENCE_PACKET_CHARS - 2_500);
427
+ const guided = withMcpAgentGuidance(bounded, "existing_scan_retrieved");
428
+ return toToolResult(guided, pulseReportText(guided, "CertScore evidence result"));
202
429
  }
203
430
  catch (error) {
204
431
  return toToolError(error);
205
432
  }
206
433
  });
207
- registerTool("get_scan_bundle", toolContract("get_scan_bundle"), async ({ scanId, maxFindings, maxPreConsentRows }) => {
434
+ registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }, extra) => {
435
+ const client = clientForRequest(extra);
208
436
  try {
209
- const [scan, report, evidence, findings, preConsentCookiesTrackers] = await Promise.all([
210
- client.scans.get(scanId),
211
- client.getScan(scanId, { detail: "summary", format: "json" }),
212
- client.getScan(scanId, { detail: "evidence", format: "json" }),
213
- client.findings.list(scanId),
214
- client.scans.preConsentCookiesTrackers(scanId)
437
+ const responseCeilingBytes = options.toolProfile === "light"
438
+ ? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
439
+ : 200_000;
440
+ const requestedMaxBytes = maxBytes ?? (options.toolProfile === "light"
441
+ ? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
442
+ : 50_000);
443
+ const internalMcpOperation = { operation: "scan_bundle", scanId };
444
+ const scan = await retryTransientOriginFailure(() => client.scans.get(scanId, { internalMcpOperation }));
445
+ if (scan.status === "completed_limited" && scan.resultDisposition === "no_go") {
446
+ const bundle = buildScanBundle({
447
+ detail,
448
+ evidence: null,
449
+ findings: { type: "certscore_finding_list", scanId, findings: [] },
450
+ maxBytes: requestedMaxBytes,
451
+ maxFindings,
452
+ maxPreConsentRows,
453
+ preConsentCookiesTrackers: null,
454
+ report: null,
455
+ requestedMaxBytes,
456
+ responseCeilingBytes,
457
+ scan
458
+ });
459
+ return toToolResult(bundle, scanBundleText(bundle));
460
+ }
461
+ const includeEvidence = detail === "evidence" || detail === "full";
462
+ const reportDetail = detail === "full" ? "full" : includeEvidence ? "evidence" : "summary";
463
+ const [report, findings, preConsentCookiesTrackers] = await Promise.all([
464
+ retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json", internalMcpOperation })),
465
+ retryTransientOriginFailure(() => client.findings.list(scanId, { internalMcpOperation })),
466
+ scan.status === "completed"
467
+ ? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId, { internalMcpOperation }))
468
+ : Promise.resolve(null)
215
469
  ]);
216
- return toToolResult(buildScanBundle({
470
+ const evidence = includeEvidence ? report : null;
471
+ const bundle = buildScanBundle({
472
+ detail,
217
473
  evidence,
218
474
  findings,
475
+ maxBytes: requestedMaxBytes,
219
476
  maxFindings,
220
477
  maxPreConsentRows,
221
478
  preConsentCookiesTrackers,
222
479
  report,
480
+ requestedMaxBytes,
481
+ responseCeilingBytes,
223
482
  scan
224
- }));
483
+ });
484
+ return toToolResult(bundle, scanBundleText(bundle));
225
485
  }
226
486
  catch (error) {
227
487
  return toToolError(error);
228
488
  }
229
489
  });
230
- registerTool("export_findings", toolContract("export_findings"), async ({ scanId }) => {
490
+ registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }) => {
231
491
  try {
232
492
  const report = await client.getScan(scanId, { detail: "full", format: "json" });
233
- return toToolResult(exportFindings(report));
493
+ const guided = withMcpAgentGuidance(exportFindings(report), "existing_scan_retrieved");
494
+ return toToolResult(guided, findingListText(guided, "Exported canonical projected findings"));
234
495
  }
235
496
  catch (error) {
236
497
  return toToolError(error);
237
498
  }
238
499
  });
239
- registerTool("list_findings", toolContract("list_findings"), async ({ limit, offset, scanId }) => {
500
+ registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }) => {
240
501
  try {
241
- return toToolResult(paginateFindingList(await client.findings.list(scanId), { limit, offset }));
502
+ const guided = withMcpAgentGuidance(paginateFindingList(await client.findings.list(scanId), { limit, offset }), "existing_scan_retrieved");
503
+ return toToolResult(guided, findingListText(guided));
242
504
  }
243
505
  catch (error) {
244
506
  return toToolError(error);
245
507
  }
246
508
  });
247
- registerTool("get_pre_consent_cookies_trackers", toolContract("get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
509
+ registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
248
510
  try {
249
- return toToolResult(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }));
511
+ const guided = withMcpAgentGuidance(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }), "existing_scan_retrieved");
512
+ return toToolResult(guided, preConsentInventoryText(guided));
250
513
  }
251
514
  catch (error) {
252
515
  return toToolError(error);
253
516
  }
254
517
  });
255
- registerTool("explain_finding", toolContract("explain_finding"), async ({ scanId, findingId }) => {
518
+ registerTool("certscore_explain_finding", toolContract("certscore_explain_finding"), async ({ scanId, findingId }) => {
256
519
  try {
257
520
  return toToolResult(await client.findings.explain(scanId, findingId));
258
521
  }
@@ -260,7 +523,7 @@ export function createCertScoreMcpServer(options = {}) {
260
523
  return toToolError(error);
261
524
  }
262
525
  });
263
- registerTool("get_latest_domain_scan", toolContract("get_latest_domain_scan"), async ({ domain, scanFrom }) => {
526
+ registerTool("certscore_get_latest_domain_scan", toolContract("certscore_get_latest_domain_scan"), async ({ domain, scanFrom }) => {
264
527
  try {
265
528
  return toToolResult(await client.domains.latest(domain, { scanFrom }));
266
529
  }
@@ -268,9 +531,10 @@ export function createCertScoreMcpServer(options = {}) {
268
531
  return toToolError(error);
269
532
  }
270
533
  });
271
- registerTool("get_latest_domain_pre_consent_cookies_trackers", toolContract("get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
534
+ registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
272
535
  try {
273
- return toToolResult(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }));
536
+ const guided = withMcpAgentGuidance(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }), "existing_scan_retrieved");
537
+ return toToolResult(guided, preConsentInventoryText(guided));
274
538
  }
275
539
  catch (error) {
276
540
  return toToolError(error);