@certscore/mcp 0.2.11 → 0.2.15

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/tools.js CHANGED
@@ -4,6 +4,18 @@ export const MAX_EVIDENCE_PACKET_CHARS = 250_000;
4
4
  const EVIDENCE_STRING_CHARS = 4_000;
5
5
  const EVIDENCE_ARRAY_ITEMS = 40;
6
6
  const EVIDENCE_OBJECT_KEYS = 80;
7
+ const OBSERVATION_ONLY_DISCLAIMER = "Observation only: no-go, not-observed, and limited-coverage results are not proof of compliance.";
8
+ function toolResultSummary(payload) {
9
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
10
+ return "CertScore tool call completed. Read structuredContent for the result.";
11
+ }
12
+ const record = payload;
13
+ const type = typeof record.type === "string" ? record.type : "result";
14
+ const status = typeof record.status === "string" ? `; status=${record.status}` : "";
15
+ const scanId = typeof record.scanId === "string" ? `; scanId=${record.scanId}` : "";
16
+ const score = typeof record.score === "number" ? `; score=${record.score}` : "";
17
+ return `CertScore ${type}${status}${scanId}${score}. Full result is in structuredContent.`;
18
+ }
7
19
  export function toToolResult(payload) {
8
20
  const structuredContent = payload !== null && typeof payload === "object" && !Array.isArray(payload)
9
21
  ? payload
@@ -13,37 +25,167 @@ export function toToolResult(payload) {
13
25
  content: [
14
26
  {
15
27
  type: "text",
16
- text: JSON.stringify(payload)
28
+ text: toolResultSummary(payload)
17
29
  }
18
30
  ]
19
31
  };
20
32
  }
21
33
  export function toToolError(error) {
22
- const payload = error instanceof CertScoreError
23
- ? {
24
- error: {
34
+ const responseRecord = error instanceof CertScoreError && error.responseBody && typeof error.responseBody === "object" && !Array.isArray(error.responseBody)
35
+ ? error.responseBody
36
+ : null;
37
+ const terminalError = responseRecord?.error && typeof responseRecord.error === "object" && !Array.isArray(responseRecord.error)
38
+ ? responseRecord.error
39
+ : null;
40
+ const status = error instanceof CertScoreError ? error.status : undefined;
41
+ const retryable = typeof terminalError?.retryable === "boolean"
42
+ ? terminalError.retryable
43
+ : status === 429 || (typeof status === "number" && status >= 500);
44
+ const retryAfterSeconds = typeof terminalError?.retryAfterSeconds === "number"
45
+ ? terminalError.retryAfterSeconds
46
+ : error instanceof CertScoreError && "retryAfterSeconds" in error && typeof error.retryAfterSeconds === "number"
47
+ ? error.retryAfterSeconds
48
+ : retryable
49
+ ? 30
50
+ : null;
51
+ const code = error instanceof CertScoreError ? error.code : "internal_error";
52
+ const message = error instanceof Error ? error.message : "Unknown CertScore MCP error.";
53
+ const recommendedNextAction = typeof terminalError?.recommendedNextAction === "string"
54
+ ? terminalError.recommendedNextAction
55
+ : retryable
56
+ ? `Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. Stop and contact CertScore support if the error repeats.`
57
+ : "Correct the request using the error details, then retry only if the requested operation is still appropriate.";
58
+ const payload = {
59
+ error: {
60
+ code,
61
+ message,
62
+ retryable,
63
+ retryAfterSeconds,
64
+ recommendedNextAction,
65
+ ...(error instanceof CertScoreError ? {
25
66
  name: error.name,
26
- message: error.message,
27
67
  status: error.status,
28
- code: error.code,
29
- retryAfterSeconds: "retryAfterSeconds" in error ? error.retryAfterSeconds : undefined,
30
68
  responseBody: truncateErrorResponseBody(error.responseBody)
31
- }
69
+ } : {})
32
70
  }
33
- : {
34
- error: {
35
- name: error instanceof Error ? error.name : "Error",
36
- message: error instanceof Error ? error.message : "Unknown CertScore MCP error."
37
- }
38
- };
39
- // MCP validates structuredContent against the tool's success output schema,
40
- // including for isError results. Keep errors in text content so a bounded
41
- // error envelope cannot be rejected as an invalid success resource.
71
+ };
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify(payload) }],
74
+ isError: true
75
+ };
76
+ }
77
+ export function toInvalidArgumentsToolError(errorMessage) {
78
+ const tool = errorMessage.match(/tool ([a-z_]+)/i)?.[1] ?? null;
79
+ const field = errorMessage.match(/\bat ([a-zA-Z0-9_.-]+)/)?.[1]
80
+ ?? (errorMessage.includes("url") ? "url" : errorMessage.includes("scanId") ? "scanId" : null);
81
+ const missing = /required|expected string, received undefined/i.test(errorMessage);
82
+ const message = field
83
+ ? missing
84
+ ? `The ${field} field is required.`
85
+ : `The ${field} field is invalid.`
86
+ : `The arguments for ${tool ?? "this tool"} are invalid.`;
87
+ const recommendedNextAction = field === "url"
88
+ ? "Provide a public URL or domain."
89
+ : field === "scanId"
90
+ ? "Provide the stable scanId returned by certscore_scan_site."
91
+ : "Correct the named fields using the tool input schema, then retry.";
92
+ const error = {
93
+ code: "invalid_arguments",
94
+ message,
95
+ ...(field ? { field } : {}),
96
+ retryable: false,
97
+ retryAfterSeconds: null,
98
+ recommendedNextAction,
99
+ mcpCode: -32602
100
+ };
101
+ const payload = tool === "certscore_scan_site"
102
+ ? {
103
+ type: "certscore_tool_error",
104
+ status: "invalid_arguments",
105
+ error,
106
+ recommendedNextTool: null,
107
+ recommendedNextAction,
108
+ observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
109
+ }
110
+ : { error };
42
111
  return {
43
112
  content: [{ type: "text", text: JSON.stringify(payload) }],
113
+ ...(tool === "certscore_scan_site" ? { structuredContent: payload } : {}),
44
114
  isError: true
45
115
  };
46
116
  }
117
+ function terminalErrorForResult(value) {
118
+ const existing = value.error && typeof value.error === "object" && !Array.isArray(value.error)
119
+ ? value.error
120
+ : null;
121
+ if (existing) {
122
+ const retryable = existing.retryable === true;
123
+ return {
124
+ code: typeof existing.code === "string" ? existing.code : String(value.status ?? "scan_failed"),
125
+ message: typeof existing.message === "string" ? existing.message : "The scan did not produce a canonical result.",
126
+ retryable,
127
+ retryAfterSeconds: typeof existing.retryAfterSeconds === "number" ? existing.retryAfterSeconds : retryable ? 30 : null,
128
+ recommendedNextAction: typeof existing.recommendedNextAction === "string"
129
+ ? existing.recommendedNextAction
130
+ : retryable
131
+ ? "Wait for the recommended delay, then retry certscore_scan_site with freshness=refresh."
132
+ : "Stop and review the scan limitations before deciding whether to change the URL."
133
+ };
134
+ }
135
+ if (value.status === "completed_limited" && value.noGo) {
136
+ const retryable = value.noGo.retryLikelyToHelp === true;
137
+ return {
138
+ code: typeof value.noGo.reasonCode === "string" ? value.noGo.reasonCode : "completed_limited",
139
+ message: typeof value.noGo.explanation === "string" ? value.noGo.explanation : "The scan completed with a no-go limitation.",
140
+ retryable,
141
+ retryAfterSeconds: retryable ? 30 : null,
142
+ recommendedNextAction: typeof value.noGo.recommendedNextAction === "string"
143
+ ? value.noGo.recommendedNextAction
144
+ : "Review the retained limitation and change the URL or site state before retrying."
145
+ };
146
+ }
147
+ const fallback = {
148
+ failed: {
149
+ code: "scanner_runtime_failure",
150
+ message: "The scan ended before a canonical result could be produced.",
151
+ retryable: true,
152
+ retryAfterSeconds: 30,
153
+ recommendedNextAction: "Wait 30 seconds, then retry certscore_scan_site with freshness=refresh. Stop if the failure repeats."
154
+ },
155
+ expired: {
156
+ code: "scan_expired",
157
+ message: "The scan expired before a canonical result was available.",
158
+ retryable: true,
159
+ retryAfterSeconds: 30,
160
+ recommendedNextAction: "Wait 30 seconds, then retry certscore_scan_site with freshness=refresh."
161
+ },
162
+ rate_limited: {
163
+ code: "rate_limited",
164
+ message: "The scan is rate limited.",
165
+ retryable: true,
166
+ retryAfterSeconds: typeof value.retryAfterSeconds === "number" ? value.retryAfterSeconds : 30,
167
+ recommendedNextAction: "Wait for the recommended delay, then retry the same certscore_scan_site request."
168
+ }
169
+ };
170
+ return fallback[String(value.status)] ?? null;
171
+ }
172
+ export function withMcpAgentGuidance(value) {
173
+ const status = String(value.status ?? "");
174
+ const active = status === "queued" || status === "running" || status === "finalizing";
175
+ const usable = status === "completed" || status === "completed_limited";
176
+ const error = terminalErrorForResult(value);
177
+ return {
178
+ ...value,
179
+ error,
180
+ recommendedNextTool: active ? "certscore_get_scan_status" : usable ? "certscore_get_scan_bundle" : null,
181
+ recommendedNextAction: error?.recommendedNextAction ?? value.recommendedNextAction ?? (active
182
+ ? `Poll certscore_get_scan_status with scanId ${value.scanId ?? value.jobId} after the recommended delay.`
183
+ : usable
184
+ ? `Call certscore_get_scan_bundle with scanId ${value.scanId ?? value.jobId} for the canonical findings and limitations.`
185
+ : "Review the result and retained limitations."),
186
+ observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
187
+ };
188
+ }
47
189
  export function boundEvidencePacket(payload, maxSerializedChars = MAX_EVIDENCE_PACKET_CHARS) {
48
190
  const originalSerializedChars = measureSerializedChars(payload);
49
191
  if (originalSerializedChars <= maxSerializedChars) {
@@ -87,6 +229,121 @@ export function boundEvidencePacket(payload, maxSerializedChars = MAX_EVIDENCE_P
87
229
  function measureSerializedChars(value) {
88
230
  return JSON.stringify(value)?.length ?? 0;
89
231
  }
232
+ function measureSerializedBytes(value) {
233
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
234
+ }
235
+ function updateBundleActualBytes(bundle) {
236
+ bundle.mcpMetadata.actualBytes = 0;
237
+ for (let attempt = 0; attempt < 3; attempt += 1) {
238
+ const measured = measureSerializedBytes(bundle);
239
+ if (bundle.mcpMetadata.actualBytes === measured) {
240
+ break;
241
+ }
242
+ bundle.mcpMetadata.actualBytes = measured;
243
+ }
244
+ return bundle.mcpMetadata.actualBytes;
245
+ }
246
+ function boundedText(value, maxChars) {
247
+ if (typeof value !== "string") {
248
+ return value;
249
+ }
250
+ return value.length > maxChars ? `${value.slice(0, maxChars)}…[truncated]` : value;
251
+ }
252
+ function compactBundleFinding(finding) {
253
+ const evidence = finding.evidence && typeof finding.evidence === "object" && !Array.isArray(finding.evidence)
254
+ ? finding.evidence
255
+ : {};
256
+ return {
257
+ type: finding.type,
258
+ id: finding.id,
259
+ scanId: finding.scanId,
260
+ label: boundedText(finding.label, 140),
261
+ criticality: finding.criticality,
262
+ confidence: finding.confidence,
263
+ plainEnglish: boundedText(finding.plainEnglish, 280),
264
+ ...(finding.resultDisposition ? { resultDisposition: finding.resultDisposition } : {}),
265
+ ...(finding.noGo ? { noGo: finding.noGo } : {}),
266
+ reviewLenses: Array.isArray(finding.reviewLenses) ? finding.reviewLenses.slice(0, 2) : [],
267
+ evidence: {
268
+ basis: evidence.basis,
269
+ summary: boundedText(evidence.summary, 220),
270
+ ...(evidence.phase !== undefined ? { phase: evidence.phase } : {}),
271
+ exampleCount: evidence.exampleCount,
272
+ examplesShown: evidence.examplesShown,
273
+ ...(evidence.hasTimingAnchor !== undefined ? { hasTimingAnchor: evidence.hasTimingAnchor } : {}),
274
+ ...(evidence.hasVendorAnchor !== undefined ? { hasVendorAnchor: evidence.hasVendorAnchor } : {}),
275
+ ...(evidence.hasConsentContext !== undefined ? { hasConsentContext: evidence.hasConsentContext } : {}),
276
+ ...(evidence.hasPolicyAnchor !== undefined ? { hasPolicyAnchor: evidence.hasPolicyAnchor } : {})
277
+ },
278
+ ...(finding.nextStep !== undefined ? { nextStep: boundedText(finding.nextStep, 180) } : {})
279
+ };
280
+ }
281
+ function compactPriorityEvidenceSummary(summary) {
282
+ const firstDigest = Array.isArray(summary.digests) ? summary.digests[0] : null;
283
+ const firstReference = summary.references && typeof summary.references === "object" && !Array.isArray(summary.references)
284
+ ? Object.entries(summary.references).find(([, value]) => typeof value === "string")
285
+ : null;
286
+ return {
287
+ digests: firstDigest ? [{
288
+ findingId: firstDigest.findingId,
289
+ basis: firstDigest.basis ?? null,
290
+ summary: boundedText(firstDigest.summary, 180) ?? null,
291
+ phase: firstDigest.phase ?? null,
292
+ evidenceUrl: firstDigest.evidenceUrl ?? firstReference?.[1] ?? null
293
+ }] : [],
294
+ evidenceAvailable: summary.evidenceAvailable === true,
295
+ evidenceSafetyNotes: [],
296
+ references: firstDigest ? {} : firstReference ? { [firstReference[0]]: firstReference[1] } : {}
297
+ };
298
+ }
299
+ function bundleEvidenceSummary(evidence, findings, links, includeDiagnostics) {
300
+ const digests = findings.slice(0, 3).map((finding) => {
301
+ const findingEvidence = finding.evidence && typeof finding.evidence === "object" && !Array.isArray(finding.evidence)
302
+ ? finding.evidence
303
+ : {};
304
+ return {
305
+ findingId: finding.id,
306
+ basis: findingEvidence.basis ?? null,
307
+ summary: boundedText(findingEvidence.summary, 500) ?? null,
308
+ phase: findingEvidence.phase ?? null,
309
+ evidenceUrl: findingEvidence.excerpt && typeof findingEvidence.excerpt === "object" && !Array.isArray(findingEvidence.excerpt)
310
+ ? findingEvidence.excerpt.evidenceUrl ?? null
311
+ : typeof links.findings === "string"
312
+ ? links.findings
313
+ : typeof links.report === "string"
314
+ ? links.report
315
+ : null
316
+ };
317
+ });
318
+ return {
319
+ digests,
320
+ evidenceAvailable: digests.length > 0 || Object.keys(evidence).length > 0,
321
+ evidenceSafetyNotes: Array.isArray(evidence.evidenceSafetyNotes)
322
+ ? evidence.evidenceSafetyNotes.slice(0, 3).map((note) => boundedText(note, 300))
323
+ : [],
324
+ references: Object.fromEntries(Object.entries(links).filter(([key, value]) => ["findings", "pulse", "report", "preConsentCookiesTrackers"].includes(key) && typeof value === "string")),
325
+ ...(includeDiagnostics ? {
326
+ projectionDiagnostics: compactEvidenceValue(evidence.projectionDiagnostics ?? null, {
327
+ arrayItems: 12,
328
+ depth: 4,
329
+ objectKeys: 24,
330
+ stringChars: 600
331
+ }),
332
+ coverageDiagnostics: compactEvidenceValue(evidence.coverageDiagnostics ?? null, {
333
+ arrayItems: 12,
334
+ depth: 4,
335
+ objectKeys: 24,
336
+ stringChars: 600
337
+ }),
338
+ policySurfaceCoverage: compactEvidenceValue(evidence.policySurfaceCoverage ?? null, {
339
+ arrayItems: 12,
340
+ depth: 4,
341
+ objectKeys: 24,
342
+ stringChars: 600
343
+ })
344
+ } : {})
345
+ };
346
+ }
90
347
  function compactEvidenceValue(value, options) {
91
348
  if (value === null || value === undefined) {
92
349
  return value;
@@ -298,71 +555,276 @@ export function limitPreConsentRows(payload, options = {}) {
298
555
  };
299
556
  }
300
557
  export function buildScanBundle(input) {
301
- const maxFindings = Math.min(50, Math.max(1, input.maxFindings ?? 20));
558
+ const detail = input.detail ?? "summary";
559
+ const maxFindings = Math.min(50, Math.max(1, input.maxFindings ?? (detail === "summary" ? 5 : 20)));
302
560
  const maxPreConsentRows = Math.min(50, Math.max(1, input.maxPreConsentRows ?? 20));
303
- const evidence = input.evidence;
304
- const report = input.report;
305
- const findings = Array.isArray(input.findings.findings) ? input.findings.findings.slice(0, maxFindings) : [];
306
- const preConsentRows = Array.isArray(input.preConsentCookiesTrackers.rows)
561
+ const evidence = (input.evidence ?? {});
562
+ const report = (input.report ?? {});
563
+ const allFindings = Array.isArray(input.findings.findings) ? input.findings.findings : [];
564
+ const findings = detail === "summary"
565
+ ? []
566
+ : allFindings.slice(0, maxFindings).map((finding) => detail === "full" ? finding : compactBundleFinding(finding));
567
+ const preConsentRows = Array.isArray(input.preConsentCookiesTrackers?.rows)
307
568
  ? input.preConsentCookiesTrackers.rows.slice(0, maxPreConsentRows)
308
569
  : [];
309
570
  const links = {
310
571
  ...(input.scan.links ?? {}),
311
572
  ...(report.links && typeof report.links === "object" && !Array.isArray(report.links) ? report.links : {})
312
573
  };
313
- return {
574
+ const maxBytes = Math.min(200_000, Math.max(5_000, input.maxBytes ?? 50_000));
575
+ const contentUrls = Object.fromEntries(Object.entries({
576
+ report: input.scan.links?.report ?? links.report,
577
+ findings: links.findings,
578
+ evidence: links.pulse,
579
+ preConsentCookiesTrackers: links.preConsentCookiesTrackers
580
+ }).filter(([, value]) => typeof value === "string"));
581
+ const intentionallyOmitted = new Set();
582
+ if (detail === "summary" && allFindings.length > 0)
583
+ intentionallyOmitted.add("findings");
584
+ if ((detail === "summary" || detail === "findings") && (Object.keys(evidence).length > 0 || allFindings.length > 0))
585
+ intentionallyOmitted.add("evidence");
586
+ if (detail !== "full" && Object.keys(report).length > 0)
587
+ intentionallyOmitted.add("fullReport");
588
+ const guidedScan = withMcpAgentGuidance(input.scan);
589
+ const bundle = {
314
590
  type: "certscore_scan_bundle",
591
+ detail,
315
592
  scanId: input.scan.scanId,
593
+ domain: input.scan.domain,
594
+ url: input.scan.url ?? null,
316
595
  status: input.scan.status,
596
+ score: input.scan.score ?? null,
597
+ scoreStatus: input.scan.scoreStatus ?? "final",
598
+ scoreVersion: input.scan.scoreVersion ?? null,
599
+ scoreUpdatedAt: input.scan.scoreUpdatedAt ?? null,
600
+ riskLevel: input.scan.riskLevel ?? null,
317
601
  resultDisposition: input.scan.resultDisposition ?? null,
318
602
  noGo: input.scan.noGo ?? null,
319
- scan: input.scan,
603
+ coverage: input.scan.coverage ?? null,
604
+ createdAt: input.scan.createdAt ?? null,
605
+ startedAt: input.scan.startedAt ?? null,
606
+ completedAt: input.scan.completedAt ?? null,
607
+ scanTimeSeconds: input.scan.scanTimeSeconds ?? null,
608
+ timing: {
609
+ createdAt: input.scan.createdAt ?? null,
610
+ startedAt: input.scan.startedAt ?? null,
611
+ completedAt: input.scan.completedAt ?? null,
612
+ scanTimeSeconds: input.scan.scanTimeSeconds ?? null
613
+ },
320
614
  summary: {
321
- summary: report.summary ?? null,
615
+ headline: report.summary && typeof report.summary === "object" && !Array.isArray(report.summary)
616
+ ? report.summary.headline ?? null
617
+ : null,
322
618
  executiveSummary: report.executiveSummary ?? null,
323
619
  counts: report.counts ?? null,
324
- coverage: report.coverage ?? input.scan.coverage ?? null,
325
620
  agentInterpretation: report.agentInterpretation ?? null
326
621
  },
327
622
  findings,
328
623
  findingsMetadata: {
329
624
  shown: findings.length,
330
- total: Array.isArray(input.findings.findings) ? input.findings.findings.length : 0,
331
- truncated: Array.isArray(input.findings.findings) && input.findings.findings.length > findings.length
625
+ total: allFindings.length,
626
+ truncated: allFindings.length > findings.length
332
627
  },
333
- evidenceSummary: {
334
- evidenceSafetyNotes: evidence.evidenceSafetyNotes ?? null,
335
- projectionDiagnostics: evidence.projectionDiagnostics ?? null,
336
- projectedFindings: compactEvidenceValue(evidence.projectedFindings ?? [], {
337
- arrayItems: maxFindings,
338
- depth: 5,
339
- objectKeys: 40,
340
- stringChars: 1_000
341
- }),
342
- coverageDiagnostics: compactEvidenceValue(evidence.coverageDiagnostics ?? null, {
343
- arrayItems: 20,
344
- depth: 5,
345
- objectKeys: 40,
346
- stringChars: 1_000
347
- }),
348
- policySurfaceCoverage: compactEvidenceValue(evidence.policySurfaceCoverage ?? null, {
349
- arrayItems: 20,
350
- depth: 5,
351
- objectKeys: 40,
352
- stringChars: 1_000
628
+ ...(detail === "evidence" || detail === "full"
629
+ ? { evidenceSummary: bundleEvidenceSummary(evidence, allFindings, links, detail === "full") }
630
+ : {}),
631
+ ...(detail === "full" ? {
632
+ fullReport: compactEvidenceValue(report, {
633
+ arrayItems: 50,
634
+ depth: 8,
635
+ objectKeys: 100,
636
+ stringChars: 4_000
353
637
  })
354
- },
355
- preConsentCookiesTrackers: {
356
- summary: input.preConsentCookiesTrackers.summary,
357
- rows: preConsentRows,
358
- shown: preConsentRows.length,
359
- total: Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.length : 0,
360
- truncated: Array.isArray(input.preConsentCookiesTrackers.rows) && input.preConsentCookiesTrackers.rows.length > preConsentRows.length
361
- },
638
+ } : {}),
639
+ ...(input.preConsentCookiesTrackers ? { preConsentCookiesTrackers: {
640
+ summary: input.preConsentCookiesTrackers.summary,
641
+ rows: preConsentRows,
642
+ shown: preConsentRows.length,
643
+ total: Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.length : 0,
644
+ truncated: Array.isArray(input.preConsentCookiesTrackers.rows) && input.preConsentCookiesTrackers.rows.length > preConsentRows.length
645
+ } } : {}),
362
646
  links,
363
- recommendedNextTool: findings.length > 0 ? "explain_finding" : null,
364
- disclaimer: input.scan.disclaimer ?? input.report.disclaimer ?? null
647
+ reportUrl: input.scan.links?.report ?? (typeof links.report === "string" ? links.report : null),
648
+ recommendedNextTool: null,
649
+ recommendedNextAction: guidedScan.error?.recommendedNextAction ?? (detail === "summary" && allFindings.length > 0
650
+ ? "Review this overview, then request detail=findings for bounded finding objects or open the report URL."
651
+ : findings.length > 0
652
+ ? "Review the returned findings and follow their evidence references. Use detail=evidence only when deeper retained context is needed."
653
+ : "Review coverage and limitations before interpreting the absence of findings."),
654
+ error: guidedScan.error,
655
+ mcpMetadata: {
656
+ detail,
657
+ heavyEvidenceIncluded: detail === "evidence" || detail === "full",
658
+ findingsTruncated: allFindings.length > findings.length,
659
+ requestedMaxBytes: maxBytes,
660
+ actualBytes: 0,
661
+ truncated: false,
662
+ truncationReason: null,
663
+ omittedSections: [...intentionallyOmitted],
664
+ nextRecommendedMaxBytes: null,
665
+ omittedContentAvailableViaUrl: intentionallyOmitted.size > 0 && Object.keys(contentUrls).length > 0,
666
+ contentUrls
667
+ },
668
+ observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER,
669
+ disclaimer: input.scan.disclaimer ?? input.report?.disclaimer ?? OBSERVATION_ONLY_DISCLAIMER
670
+ };
671
+ const recommendationCandidates = [];
672
+ const markBudgetOmitted = (section, reason, requiredBytes = bundle.mcpMetadata.actualBytes) => {
673
+ bundle.mcpMetadata.truncated = true;
674
+ bundle.mcpMetadata.truncationReason ??= reason;
675
+ if (!bundle.mcpMetadata.omittedSections.includes(section)) {
676
+ bundle.mcpMetadata.omittedSections.push(section);
677
+ }
678
+ recommendationCandidates.push(requiredBytes);
679
+ bundle.mcpMetadata.omittedContentAvailableViaUrl = Object.keys(contentUrls).length > 0;
365
680
  };
681
+ const refresh = () => updateBundleActualBytes(bundle);
682
+ refresh();
683
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.fullReport) {
684
+ markBudgetOmitted("fullReport", "full_report_omitted_to_byte_limit");
685
+ delete bundle.fullReport;
686
+ refresh();
687
+ }
688
+ if (bundle.mcpMetadata.actualBytes > maxBytes && detail === "full" && bundle.evidenceSummary) {
689
+ const compactSummary = bundleEvidenceSummary(evidence, allFindings, links, false);
690
+ markBudgetOmitted("evidenceDiagnostics", "evidence_diagnostics_omitted_to_byte_limit");
691
+ bundle.evidenceSummary = compactSummary;
692
+ refresh();
693
+ }
694
+ const inventoryRows = bundle.preConsentCookiesTrackers?.rows;
695
+ while (bundle.mcpMetadata.actualBytes > maxBytes && Array.isArray(inventoryRows) && inventoryRows.length > 0) {
696
+ markBudgetOmitted("preConsentCookiesTrackers", "evidence_inventory_reduced_to_byte_limit");
697
+ inventoryRows.pop();
698
+ bundle.preConsentCookiesTrackers.shown = inventoryRows.length;
699
+ bundle.preConsentCookiesTrackers.truncated = true;
700
+ refresh();
701
+ }
702
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.preConsentCookiesTrackers) {
703
+ markBudgetOmitted("preConsentCookiesTrackers", "evidence_inventory_omitted_to_byte_limit");
704
+ delete bundle.preConsentCookiesTrackers;
705
+ refresh();
706
+ }
707
+ while (bundle.mcpMetadata.actualBytes > maxBytes && bundle.findings.length > 1) {
708
+ markBudgetOmitted("additionalFindings", "findings_reduced_to_byte_limit");
709
+ bundle.findings.pop();
710
+ bundle.findingsMetadata.shown = bundle.findings.length;
711
+ bundle.findingsMetadata.truncated = true;
712
+ bundle.mcpMetadata.findingsTruncated = true;
713
+ refresh();
714
+ }
715
+ if (bundle.mcpMetadata.actualBytes > maxBytes) {
716
+ markBudgetOmitted("summaryDetail", "summary_compacted_to_byte_limit");
717
+ bundle.summary = {
718
+ headline: boundedText(bundle.summary?.headline, 240) ?? null,
719
+ executiveSummary: null,
720
+ counts: bundle.summary?.counts ? { totalAutomatedFindingCount: bundle.findingsMetadata.total } : null,
721
+ agentInterpretation: null
722
+ };
723
+ refresh();
724
+ }
725
+ if (bundle.mcpMetadata.actualBytes > maxBytes) {
726
+ markBudgetOmitted("coverageDetail", "coverage_compacted_to_byte_limit");
727
+ bundle.coverage = compactEvidenceValue(bundle.coverage, {
728
+ arrayItems: 3,
729
+ depth: 3,
730
+ objectKeys: 8,
731
+ stringChars: 240
732
+ });
733
+ refresh();
734
+ }
735
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.links) {
736
+ markBudgetOmitted("additionalLinks", "links_compacted_to_byte_limit");
737
+ bundle.links = Object.fromEntries(Object.entries(bundle.links).filter(([key]) => ["self", "report"].includes(key)));
738
+ refresh();
739
+ }
740
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.evidenceSummary) {
741
+ markBudgetOmitted("evidenceDetail", "evidence_compacted_to_preserve_priority_content");
742
+ bundle.evidenceSummary = compactPriorityEvidenceSummary(bundle.evidenceSummary);
743
+ refresh();
744
+ }
745
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.disclaimer === OBSERVATION_ONLY_DISCLAIMER) {
746
+ markBudgetOmitted("duplicateDisclaimer", "duplicate_disclaimer_omitted_to_byte_limit");
747
+ bundle.disclaimer = null;
748
+ refresh();
749
+ }
750
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.findings.length > 0) {
751
+ markBudgetOmitted("findings", "finding_omitted_to_byte_limit");
752
+ bundle.findings = [];
753
+ bundle.findingsMetadata.shown = 0;
754
+ bundle.findingsMetadata.truncated = bundle.findingsMetadata.total > 0;
755
+ bundle.mcpMetadata.findingsTruncated = bundle.findingsMetadata.total > 0;
756
+ refresh();
757
+ }
758
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.evidenceSummary) {
759
+ markBudgetOmitted("evidence", "evidence_digest_omitted_to_byte_limit");
760
+ delete bundle.evidenceSummary;
761
+ bundle.mcpMetadata.heavyEvidenceIncluded = false;
762
+ refresh();
763
+ }
764
+ if (bundle.mcpMetadata.truncated) {
765
+ const required = Math.min(...recommendationCandidates.filter((value) => Number.isFinite(value) && value > maxBytes));
766
+ bundle.mcpMetadata.nextRecommendedMaxBytes = Math.min(200_000, Math.max(maxBytes + 1_000, Math.ceil((Number.isFinite(required) ? required : maxBytes + 1_000) / 1_000) * 1_000));
767
+ bundle.recommendedNextAction = `Retry with maxBytes=${bundle.mcpMetadata.nextRecommendedMaxBytes} or open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`;
768
+ refresh();
769
+ }
770
+ if (bundle.mcpMetadata.actualBytes > maxBytes) {
771
+ const minimal = {
772
+ type: bundle.type,
773
+ detail: bundle.detail,
774
+ scanId: bundle.scanId,
775
+ domain: bundle.domain,
776
+ url: bundle.url,
777
+ status: bundle.status,
778
+ score: bundle.score,
779
+ scoreStatus: bundle.scoreStatus,
780
+ scoreVersion: bundle.scoreVersion,
781
+ scoreUpdatedAt: bundle.scoreUpdatedAt,
782
+ riskLevel: bundle.riskLevel,
783
+ resultDisposition: bundle.resultDisposition,
784
+ noGo: bundle.noGo,
785
+ coverage: null,
786
+ createdAt: bundle.createdAt,
787
+ startedAt: bundle.startedAt,
788
+ completedAt: bundle.completedAt,
789
+ scanTimeSeconds: bundle.scanTimeSeconds,
790
+ timing: bundle.timing,
791
+ summary: {
792
+ headline: bundle.summary?.headline ?? null,
793
+ executiveSummary: null,
794
+ counts: bundle.summary?.counts ?? null,
795
+ agentInterpretation: null
796
+ },
797
+ findings: [],
798
+ findingsMetadata: {
799
+ shown: 0,
800
+ total: bundle.findingsMetadata.total,
801
+ truncated: bundle.findingsMetadata.total > 0
802
+ },
803
+ links: Object.fromEntries(Object.entries(bundle.links ?? {}).filter(([key]) => ["docs", "report", "self"].includes(key))),
804
+ reportUrl: bundle.reportUrl,
805
+ recommendedNextTool: null,
806
+ recommendedNextAction: bundle.recommendedNextAction,
807
+ error: bundle.error,
808
+ mcpMetadata: {
809
+ detail,
810
+ heavyEvidenceIncluded: false,
811
+ findingsTruncated: bundle.findingsMetadata.total > 0,
812
+ requestedMaxBytes: maxBytes,
813
+ actualBytes: 0,
814
+ truncated: true,
815
+ truncationReason: "minimal_canonical_result_returned_to_byte_limit",
816
+ omittedSections: [...new Set([...bundle.mcpMetadata.omittedSections, "summaryDetail", "findings", "evidence"])],
817
+ nextRecommendedMaxBytes: bundle.mcpMetadata.nextRecommendedMaxBytes ?? Math.min(200_000, maxBytes + 1_000),
818
+ omittedContentAvailableViaUrl: Object.keys(contentUrls).length > 0,
819
+ contentUrls
820
+ },
821
+ observationOnlyDisclaimer: bundle.observationOnlyDisclaimer,
822
+ disclaimer: bundle.disclaimer
823
+ };
824
+ updateBundleActualBytes(minimal);
825
+ return minimal;
826
+ }
827
+ return bundle;
366
828
  }
367
829
  export function explainFinding(report, findingId) {
368
830
  const finding = findingsFromReport(report).find((candidate) => candidate.id === findingId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certscore/mcp",
3
- "version": "0.2.11",
3
+ "version": "0.2.15",
4
4
  "mcpName": "ai.certscore/mcp",
5
5
  "private": false,
6
6
  "description": "MCP server for CertScore public website risk-signal workflows.",
@@ -31,7 +31,8 @@
31
31
  "dist",
32
32
  "README.md",
33
33
  "LICENSE",
34
- "server.json"
34
+ "server.json",
35
+ "server-light.json"
35
36
  ],
36
37
  "publishConfig": {
37
38
  "access": "public"
@@ -63,7 +64,7 @@
63
64
  ],
64
65
  "license": "proprietary",
65
66
  "dependencies": {
66
- "@modelcontextprotocol/sdk": "^1.29.0",
67
+ "@modelcontextprotocol/sdk": "^1.30.0",
67
68
  "zod": "^3.25.76"
68
69
  },
69
70
  "devDependencies": {