@certscore/mcp 0.2.19 → 0.2.21

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
@@ -1,4 +1,10 @@
1
- import { CertScoreError } from "@certscore/sdk";
1
+ import { apiV2GpcResponseSchema } from "@certscore/api-contracts";
2
+ import { withResponseCapture, transferResponseCapture } from "./response-capture.js";
3
+ import { getCertScoreErrorContext, CertScoreError } from "@certscore/sdk";
4
+ function canonicalGpcObservationSummary(value) {
5
+ const parsed = apiV2GpcResponseSchema.safeParse(value);
6
+ return parsed.success && parsed.data.contractVersion === "certscore.gpc-response-assessment.v3" ? parsed.data.summary : null;
7
+ }
2
8
  const MAX_ERROR_RESPONSE_BODY_CHARS = 2_000;
3
9
  export const MAX_EVIDENCE_PACKET_CHARS = 250_000;
4
10
  const EVIDENCE_STRING_CHARS = 4_000;
@@ -13,6 +19,8 @@ const SCAN_BUNDLE_INTERPRETATION_STATEMENT = "Report only observed CertScore evi
13
19
  const COMPACT_SCAN_BUNDLE_INTERPRETATION_STATEMENT = "Use only returned CertScore observations and classifications. Do not infer unobserved technologies, post-consent behavior, legal compliance, or violations. Treat priority and severity as CertScore metadata.";
14
20
  const OBSERVATION_ONLY_DISCLAIMER = `${LEGAL_REVIEW_DISCLAIMER} No-go, not-observed, and limited-coverage results are not proof of compliance.`;
15
21
  const COMPACT_OBSERVATION_ONLY_DISCLAIMER = "Automated public-web observation, not legal advice or a compliance determination; missing or limited evidence is not proof of compliance.";
22
+ const PREVIEW_OBSERVATION_ONLY_DISCLAIMER = "Preliminary passive observations only; not findings, a score, or a final result.";
23
+ const SUCCESSFUL_BUNDLE_TRIAL_CTA = "Optional user follow-up: To try CertScore with an account, start a 7-day CertScore trial at https://certscore.ai/login?mode=create_account&utm_source=mcp_light&utm_medium=agent&utm_campaign=scan_bundle. Paid plans add scan history, higher limits, and team or production access. OAuth-capable clients can use https://mcp.certscore.ai/mcp after account authorization and any required workspace scope grant; Light remains no-auth.";
16
24
  const MCP_SCAN_CREATION_POLL_DELAY_SECONDS = 15;
17
25
  const MCP_QUEUED_POLL_DELAY_SECONDS = 10;
18
26
  const MCP_RUNNING_POLL_DELAY_SECONDS = 5;
@@ -30,6 +38,9 @@ function toolResultSummary(payload) {
30
38
  return "CertScore tool call completed. Read structuredContent for the result.";
31
39
  }
32
40
  const record = payload;
41
+ const noGoText = canonicalNoGoText(record);
42
+ if (noGoText)
43
+ return noGoText;
33
44
  const type = typeof record.type === "string" ? record.type : "result";
34
45
  const status = typeof record.status === "string" ? `; status=${record.status}` : "";
35
46
  const scanId = typeof record.scanId === "string" ? `; scanId=${record.scanId}` : "";
@@ -40,8 +51,9 @@ function toolResultSummary(payload) {
40
51
  const previewSummary = preview?.summary && typeof preview.summary === "object" && !Array.isArray(preview.summary)
41
52
  ? preview.summary
42
53
  : null;
54
+ const active = record.status === "queued" || record.status === "running" || record.status === "finalizing";
43
55
  const preliminary = preview
44
- ? `; preliminary pre-consent preview=cookies ${previewSummary?.cookieCount ?? "unknown"}, trackers ${previewSummary?.trackerCount ?? "unknown"}, third-party requests ${previewSummary?.thirdPartyRequestCount ?? "unknown"}; preview is not final—continue status polling`
56
+ ? `; preliminary pre-consent preview=cookies ${previewSummary?.cookieCount ?? "unknown"}, trackers ${previewSummary?.trackerCount ?? "unknown"}, third-party requests ${previewSummary?.thirdPartyRequestCount ?? "unknown"}; preview is not final—${active ? "continue status polling" : "follow the terminal result guidance"}`
45
57
  : "";
46
58
  const recordLinks = record.links && typeof record.links === "object" && !Array.isArray(record.links)
47
59
  ? record.links
@@ -68,7 +80,7 @@ export function toToolResult(payload, text) {
68
80
  const structuredContent = payload !== null && typeof payload === "object" && !Array.isArray(payload)
69
81
  ? payload
70
82
  : { value: payload };
71
- return {
83
+ return transferResponseCapture(payload, {
72
84
  structuredContent,
73
85
  content: [
74
86
  {
@@ -76,9 +88,9 @@ export function toToolResult(payload, text) {
76
88
  text: text ?? toolResultSummary(payload)
77
89
  }
78
90
  ]
79
- };
91
+ });
80
92
  }
81
- export function toToolError(error) {
93
+ export function toToolError(error, context = {}) {
82
94
  const responseRecord = error instanceof CertScoreError && error.responseBody && typeof error.responseBody === "object" && !Array.isArray(error.responseBody)
83
95
  ? error.responseBody
84
96
  : null;
@@ -97,26 +109,33 @@ export function toToolError(error) {
97
109
  ? 30
98
110
  : null;
99
111
  const code = error instanceof CertScoreError ? error.code : "internal_error";
100
- const reasonCode = terminalError?.reasonCode === "non_public_target" ? "non_public_target" : null;
101
- const message = error instanceof Error ? error.message : "Unknown CertScore MCP error.";
112
+ const reasonCode = typeof terminalError?.reasonCode === "string" && ["non_public_target", "domain_not_found", "dns_unavailable"].includes(terminalError.reasonCode) ? terminalError.reasonCode : null;
113
+ const targetRejected = context.scanCreation === true && code === "invalid_url" && status === 400;
114
+ const originalMessage = error instanceof Error ? error.message : "Unknown CertScore MCP error.";
115
+ const message = targetRejected ? `Scan target rejected. No scan was started. ${originalMessage}` : originalMessage;
102
116
  const creationRateLimit = terminalError?.creationRateLimit && typeof terminalError.creationRateLimit === "object" && !Array.isArray(terminalError.creationRateLimit)
103
117
  ? terminalError.creationRateLimit
104
118
  : null;
105
- const recommendedNextAction = typeof terminalError?.recommendedNextAction === "string"
106
- ? terminalError.recommendedNextAction
107
- : creationRateLimit
108
- ? `No scan was created. Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. If the limit continues after that delay, contact support@certscore.ai.`
109
- : retryable
110
- ? `Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. Stop and contact support@certscore.ai if the error repeats.`
111
- : "Correct the request using the error details, then retry only if the requested operation is still appropriate.";
119
+ const recommendedNextAction = targetRejected
120
+ ? reasonCode === "non_public_target"
121
+ ? "Ask for a publicly reachable HTTP or HTTPS website, then call certscore_scan_site with that URL. Do not retry this private or ineligible target or try to bypass the public-target checks."
122
+ : 'Check the spelling and DNS of the intended hostname, then call certscore_scan_site with the actual public website URL, for example {"url":"https://example.com"}. A bare domain is accepted, but example.com and www.example.com are different hostnames; use www only if it is the intended site. Do not repeat the same invalid request. If the correct URL is unclear, ask the user.'
123
+ : typeof terminalError?.recommendedNextAction === "string"
124
+ ? terminalError.recommendedNextAction
125
+ : creationRateLimit
126
+ ? `No scan was created. Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. If the limit continues after that delay, contact support@certscore.ai.`
127
+ : retryable
128
+ ? `Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. Stop and contact support@certscore.ai if the error repeats.`
129
+ : "Correct the request using the error details, then retry only if the requested operation is still appropriate.";
112
130
  const payload = {
113
131
  error: {
114
132
  code,
115
133
  reasonCode,
116
134
  message,
117
- retryable,
118
- retryAfterSeconds,
135
+ retryable: targetRejected ? false : retryable,
136
+ retryAfterSeconds: targetRejected ? null : retryAfterSeconds,
119
137
  recommendedNextAction,
138
+ ...(targetRejected ? { field: "url", scanStarted: false, inputCorrectionRequired: true } : {}),
120
139
  ...(creationRateLimit
121
140
  ? { creationRateLimit }
122
141
  : {}),
@@ -127,26 +146,31 @@ export function toToolError(error) {
127
146
  } : {})
128
147
  }
129
148
  };
130
- return {
131
- content: [{ type: "text", text: JSON.stringify(payload) }],
149
+ return withResponseCapture({
150
+ content: [
151
+ { type: "text", text: JSON.stringify(payload) },
152
+ ...(targetRejected ? [{ type: "text", text: `${message} ${recommendedNextAction}` }] : []),
153
+ ],
132
154
  isError: true
133
- };
155
+ }, { ...(targetRejected || typeof terminalError?.recommendedNextAction !== "string" ? { recommendedNextAction } : {}), ...(getCertScoreErrorContext(error) ? { upstream: getCertScoreErrorContext(error) } : {}) });
134
156
  }
135
- export function toInvalidArgumentsToolError(errorMessage) {
136
- const tool = errorMessage.match(/tool ([a-z_]+)/i)?.[1] ?? null;
137
- const field = errorMessage.match(/\bat ([a-zA-Z0-9_.-]+)/)?.[1]
157
+ export function toInvalidArgumentsToolError(errorMessage, validation) {
158
+ const tool = validation?.tool ?? errorMessage.match(/tool ([a-z_]+)/i)?.[1] ?? null;
159
+ const field = validation?.issues[0]?.field ?? errorMessage.match(/\bat ([a-zA-Z0-9_.-]+)/)?.[1]
138
160
  ?? (errorMessage.includes("url") ? "url" : errorMessage.includes("scanId") ? "scanId" : null);
139
- const missing = /required|expected string, received undefined/i.test(errorMessage);
140
- const message = field
141
- ? missing
142
- ? `The ${field} field is required.`
143
- : `The ${field} field is invalid.`
144
- : `The arguments for ${tool ?? "this tool"} are invalid.`;
161
+ const missing = validation?.issues[0]?.required === true || /required|expected string, received undefined/i.test(errorMessage);
162
+ const message = validation && validation.issues.length > 1
163
+ ? `Invalid arguments in fields: ${validation.issues.map(issue => issue.field).join(", ")}.`
164
+ : field
165
+ ? missing
166
+ ? `The ${field} field is required.`
167
+ : `The ${field} field is invalid.`
168
+ : `The arguments for ${tool ?? "this tool"} are invalid.`;
145
169
  const recommendedNextAction = field === "url"
146
170
  ? "Provide a public URL or domain."
147
171
  : field === "scanId"
148
172
  ? "Provide the stable scanId returned by certscore_scan_site."
149
- : "Correct the named fields using the tool input schema, then retry.";
173
+ : "Correct the named fields using the tool input schema. Omit optional parameters to use defaults; do not send null. Refresh tool discovery (tools/list) for the accepted types, values and limits, then retry.";
150
174
  const error = {
151
175
  code: "invalid_arguments",
152
176
  message,
@@ -154,7 +178,8 @@ export function toInvalidArgumentsToolError(errorMessage) {
154
178
  retryable: false,
155
179
  retryAfterSeconds: null,
156
180
  recommendedNextAction,
157
- mcpCode: -32602
181
+ mcpCode: -32602,
182
+ ...(validation ? { issues: validation.issues } : {})
158
183
  };
159
184
  const payload = tool === "certscore_scan_site"
160
185
  ? {
@@ -169,28 +194,31 @@ export function toInvalidArgumentsToolError(errorMessage) {
169
194
  observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
170
195
  }
171
196
  : { error };
172
- return {
197
+ return withResponseCapture({
173
198
  content: [{ type: "text", text: JSON.stringify(payload) }],
174
199
  ...(tool === "certscore_scan_site" ? { structuredContent: payload } : {}),
175
200
  isError: true
176
- };
201
+ }, { message: error.message, recommendedNextAction: error.recommendedNextAction });
177
202
  }
178
203
  export function toInvalidScanIdToolError() {
179
204
  const error = {
180
205
  code: "invalid_scan_id",
181
206
  field: "scanId",
182
- message: "The scanId must be the canonical UUID returned by certscore_scan_site.",
207
+ message: "Invalid scanId. Use the unchanged UUID returned by certscore_scan_site. This request did not start a scan.",
183
208
  retryable: false,
184
209
  retryAfterSeconds: null,
185
210
  recommendedNextAction: "Use the unchanged scanId returned by certscore_scan_site. Do not use placeholders, report URLs, domains, or job IDs.",
186
211
  mcpCode: -32602
187
212
  };
188
- return {
213
+ return withResponseCapture({
189
214
  content: [{ type: "text", text: JSON.stringify({ error }) }],
190
215
  isError: true
191
- };
216
+ }, { message: error.message, recommendedNextAction: error.recommendedNextAction });
192
217
  }
193
218
  function terminalErrorForResult(value) {
219
+ const terminalGuidance = value.status === "failed" || value.status === "expired"
220
+ ? ` This scan is terminal; polling the same scanId will not resume it. A freshness=refresh request starts a new scan and uses scan quota. Stop if the failure repeats.${typeof value.scanId === "string" && /^[a-f0-9-]{36}$/i.test(value.scanId) ? ` Support reference: ${value.scanId}.` : ""}`
221
+ : "";
194
222
  const existing = value.error && typeof value.error === "object" && !Array.isArray(value.error)
195
223
  ? value.error
196
224
  : null;
@@ -201,11 +229,11 @@ function terminalErrorForResult(value) {
201
229
  message: typeof existing.message === "string" ? existing.message : "The scan did not produce a canonical result.",
202
230
  retryable,
203
231
  retryAfterSeconds: typeof existing.retryAfterSeconds === "number" ? existing.retryAfterSeconds : retryable ? 30 : null,
204
- recommendedNextAction: typeof existing.recommendedNextAction === "string"
232
+ recommendedNextAction: (typeof existing.recommendedNextAction === "string"
205
233
  ? existing.recommendedNextAction
206
234
  : retryable
207
235
  ? "Wait for the recommended delay, then retry certscore_scan_site with freshness=refresh."
208
- : "Stop and review the scan limitations before deciding whether to change the URL."
236
+ : "Stop and review the scan limitations before deciding whether to change the URL.") + (typeof existing.recommendedNextAction === "string" && existing.recommendedNextAction.endsWith(terminalGuidance) ? "" : terminalGuidance)
209
237
  };
210
238
  }
211
239
  if (value.status === "completed_limited" && value.noGo) {
@@ -243,7 +271,8 @@ function terminalErrorForResult(value) {
243
271
  recommendedNextAction: "Wait for the recommended delay, then retry the same certscore_scan_site request."
244
272
  }
245
273
  };
246
- return fallback[String(value.status)] ?? null;
274
+ const error = fallback[String(value.status)];
275
+ return error ? { ...error, recommendedNextAction: error.recommendedNextAction + terminalGuidance } : null;
247
276
  }
248
277
  function scanProvenance(value, fallbackMode) {
249
278
  const executionMode = value.executionMode === "new_scan" || value.executionMode === "reused_scan"
@@ -308,6 +337,12 @@ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown",
308
337
  const hasPreConsentPreview = Boolean(value.preConsentPreview &&
309
338
  typeof value.preConsentPreview === "object" &&
310
339
  !Array.isArray(value.preConsentPreview));
340
+ const preConsentPreview = hasPreConsentPreview
341
+ ? {
342
+ ...value.preConsentPreview,
343
+ observationOnlyDisclaimer: PREVIEW_OBSERVATION_ONLY_DISCLAIMER,
344
+ }
345
+ : value.preConsentPreview;
311
346
  const reportUrl = usable
312
347
  ? typeof value.reportUrl === "string" && value.reportUrl.trim()
313
348
  ? value.reportUrl.trim()
@@ -322,8 +357,9 @@ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown",
322
357
  : null;
323
358
  const activePollAction = `${retryAfterSeconds === null ? "Wait for the recommended delay" : `Wait at least ${retryAfterSeconds} seconds`}, then call certscore_get_scan_status once with scanId ${stableScanId ?? value.jobId}.`;
324
359
  const activeNextAction = `${hasPreConsentPreview ? "The returned preConsentPreview is a partial preview of passive evidence. Its counts are checkpoint-only partial counts, not the full scan tally; do not present them as final totals or stop the workflow. " : ""}${activePollAction} Continue with certscore_get_scan_status using the unchanged scanId ${stableScanId ?? value.jobId}. Do not poll in parallel or resubmit certscore_scan_site while this scan is active. After completed or completed_limited, call certscore_get_scan_bundle for the completed scan's final returned tally, canonical findings, and limitations.`;
325
- return {
360
+ return withResponseCapture({
326
361
  ...value,
362
+ preConsentPreview,
327
363
  retryAfterSeconds,
328
364
  error,
329
365
  reportUrl,
@@ -337,14 +373,14 @@ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown",
337
373
  ? `Call certscore_get_scan_bundle with scanId ${stableScanId ?? value.jobId} for the completed scan's final returned tally, canonical findings, and limitations.`
338
374
  : "Review the result and retained limitations.")),
339
375
  observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
340
- };
376
+ }, error && !value.error && !value.noGo ? { message: error.message, recommendedNextAction: error.recommendedNextAction } : {});
341
377
  }
342
378
  export function withMcpScanProvenanceGuidance(value, fallbackProvenanceMode) {
343
379
  const guided = withMcpAgentGuidance(value, fallbackProvenanceMode, "scan_status");
344
- return {
380
+ return transferResponseCapture(guided, {
345
381
  ...guided,
346
382
  interpretationGuidance: interpretationGuidance(`${INTERPRETATION_STATEMENT} ${SCAN_PROVENANCE_GROUNDING}`)
347
- };
383
+ });
348
384
  }
349
385
  export function boundEvidencePacket(payload, maxSerializedChars = MAX_EVIDENCE_PACKET_CHARS) {
350
386
  const originalSerializedChars = measureSerializedChars(payload);
@@ -991,8 +1027,37 @@ function canonicalScanProvenanceText(value) {
991
1027
  const numeric = (field) => typeof field === "number" && Number.isFinite(field) ? String(field) : "unavailable";
992
1028
  return `Canonical scan provenance: scanId=${present(extractScanId(value))}; scanFrom/execution region=${present(value.scanFrom)}; completedAt=${present(value.completedAt)}; startedAt=${present(value.startedAt)}; createdAt=${present(value.createdAt)}; retrieval mode=${present(value.provenance?.retrievalMode)}; original creation decision=${present(value.provenance?.creationDecision)}; scan age seconds=${numeric(value.provenance?.scanAgeSeconds)}; compatibility provenance mode=${present(value.provenance?.mode)}.`;
993
1029
  }
1030
+ /** Present only the API's retained disposition; never infer blockers from raw evidence. */
1031
+ function canonicalNoGo(value) {
1032
+ return value.resultDisposition === "no_go" && value.noGo && typeof value.noGo === "object" && !Array.isArray(value.noGo)
1033
+ ? value.noGo
1034
+ : null;
1035
+ }
1036
+ function canonicalNoGoText(value) {
1037
+ const noGo = canonicalNoGo(value);
1038
+ if (!noGo)
1039
+ return null;
1040
+ return [
1041
+ `CertScore scan: ${boundedText(noGo.title, 240) ?? "Scan access limitation"}; status=${value.status ?? "completed_limited"}; Not scored.`,
1042
+ boundedText(noGo.explanation, 1_200),
1043
+ boundedText(noGo.summary, 600),
1044
+ noGo.evidenceExcerpt ? `Retained evidence: ${boundedText(noGo.evidenceExcerpt, 1_200)}` : null,
1045
+ noGo.recommendedNextAction ? `Next: ${boundedText(noGo.recommendedNextAction, 1_000)}` : null,
1046
+ typeof noGo.retryLikelyToHelp === "boolean" ? `Retry likely to help: ${noGo.retryLikelyToHelp ? "yes" : "no"}.` : null,
1047
+ value.mcpMetadata?.truncated ? "Additional retained details were omitted to fit the response budget; see omission metadata and the report URL." : null,
1048
+ canonicalScanProvenanceText(value),
1049
+ `Full report: ${reportUrlFor(value) ?? "not available"}.`,
1050
+ OBSERVATION_ONLY_DISCLAIMER,
1051
+ SCAN_PROVENANCE_GROUNDING,
1052
+ ].filter(Boolean).join("\n");
1053
+ }
994
1054
  export function scanStatusText(value) {
995
- const reportUrl = reportUrlFor(value);
1055
+ const noGoText = canonicalNoGoText(value);
1056
+ if (noGoText)
1057
+ return noGoText;
1058
+ const reportUrl = value.status === "completed" || value.status === "completed_limited"
1059
+ ? reportUrlFor(value)
1060
+ : null;
996
1061
  const nextAction = typeof value.recommendedNextAction === "string" && value.recommendedNextAction.trim()
997
1062
  ? value.recommendedNextAction.trim()
998
1063
  : "Review the returned status and retained limitations.";
@@ -1008,6 +1073,9 @@ export function scanStatusText(value) {
1008
1073
  ]);
1009
1074
  }
1010
1075
  export function scanSiteText(value, leadingLines = []) {
1076
+ const noGoText = canonicalNoGoText(value);
1077
+ if (noGoText)
1078
+ return noGoText;
1011
1079
  const scanId = extractScanId(value) ?? "unknown";
1012
1080
  const active = value.status === "queued" || value.status === "running" || value.status === "finalizing";
1013
1081
  const nextAction = typeof value.recommendedNextAction === "string" && value.recommendedNextAction.trim()
@@ -1031,6 +1099,9 @@ function terminalLaneResultTextLines(value) {
1031
1099
  ? value.gpcResponse
1032
1100
  : null;
1033
1101
  if (gpcResponse) {
1102
+ const observationSummary = canonicalGpcObservationSummary(gpcResponse);
1103
+ if (observationSummary)
1104
+ lines.push(`GPC observation: ${observationSummary}`);
1034
1105
  lines.push(`GPC response: ${gpcResponse.findingTitle ?? "GPC response"}; status=${gpcResponse.status ?? "indeterminate"}; Sec-GPC: 1 proof retained on ${gpcResponse.comparison?.enabledProof?.requestsWithSecGpc ?? 0} request(s).`);
1035
1106
  }
1036
1107
  for (const [field, label] of [["postAcceptObservation", "Accept Path"], ["postRefusalObservation", "Reject Path"]]) {
@@ -1077,9 +1148,17 @@ function preConsentPreviewTextLines(value) {
1077
1148
  const returnedTrackingVendorCount = summary.returnedTrackingVendorCount ?? trackers.length;
1078
1149
  const capturedOperationalVendorCount = summary.operationalVendorCount ?? operationalVendors.length;
1079
1150
  const returnedOperationalVendorCount = summary.returnedOperationalVendorCount ?? operationalVendors.length;
1151
+ const status = String(value.status ?? "");
1152
+ const active = status === "queued" || status === "running" || status === "finalizing";
1153
+ const usable = status === "completed" || status === "completed_limited";
1154
+ const previewWorkflowGuidance = active
1155
+ ? "Wait for terminal scan status, then call certscore_get_scan_bundle for the completed scan's final returned tally, canonical findings, and coverage limitations."
1156
+ : usable
1157
+ ? "This scan is terminal. Do not use these checkpoint counts as the final tally; call certscore_get_scan_bundle for the canonical findings and coverage limitations."
1158
+ : "This scan is terminal without a completed result. Treat this preview as retained diagnostic context only, do not continue polling, and follow the terminal Next guidance.";
1080
1159
  const lines = [
1081
1160
  `Partial pre-consent runtime preview: generated=${compactPreviewValue(preview.generatedAt, "unavailable", 80)}; lane=${compactPreviewValue(preview.sourceLane, "runtime_evidence", 80)}; coverage=${compactPreviewValue(coverage.status, "unknown", 80)}; cookies captured=${capturedCookieCount}; cookie identities returned=${returnedCookieCount}; tracking vendors captured=${capturedTrackingVendorCount}; tracking vendor identities returned=${returnedTrackingVendorCount}; operational/security/consent vendors captured=${capturedOperationalVendorCount}; operational identities returned=${returnedOperationalVendorCount}; all classified vendor observations=${summary.vendorCount ?? "unknown"}; third-party requests=${summary.thirdPartyRequestCount ?? "unknown"}.`,
1082
- "PARTIAL PREVIEW: These are checkpoint-only partial counts, not the full scan tally. Do not present them as final totals or stop the workflow. Wait for terminal scan status, then call certscore_get_scan_bundle for the completed scan's final returned tally, canonical findings, and coverage limitations.",
1161
+ `PARTIAL PREVIEW: These are checkpoint-only partial counts, not the full scan tally. ${previewWorkflowGuidance}`,
1083
1162
  "Metric scope: tracking vendors exclude operational, security, and consent-management vendors. The completed inventory's broader trackerCount may include those categories, so do not compare that field directly with trackingVendorCount.",
1084
1163
  ];
1085
1164
  if (cookies.length > 0) {
@@ -1127,10 +1206,11 @@ function preConsentPreviewTextLines(value) {
1127
1206
  String(capturedOperationalVendorCount) !== String(returnedOperationalVendorCount)) {
1128
1207
  lines.push(`Preview identity lists are bounded: ${capturedCookieCount} cookies captured/${returnedCookieCount} identities returned; ${capturedTrackingVendorCount} tracking vendors captured/${returnedTrackingVendorCount} identities returned; ${capturedOperationalVendorCount} operational vendors captured/${returnedOperationalVendorCount} identities returned. Use captured counts for checkpoint coverage and returned arrays for names.`);
1129
1208
  }
1130
- const disclaimer = typeof preview.observationOnlyDisclaimer === "string" && preview.observationOnlyDisclaimer.trim()
1131
- ? preview.observationOnlyDisclaimer.trim()
1132
- : "Preliminary passive observations only; not findings, a score, or a final result.";
1133
- lines.push(`${compactPreviewValue(disclaimer, "Preliminary passive observations only.", 500)} Continue sequential status polling until terminal status; at completed or completed_limited, retrieve certscore_get_scan_bundle before reporting the full scan results or final returned tally.`);
1209
+ lines.push(`${PREVIEW_OBSERVATION_ONLY_DISCLAIMER} ${active
1210
+ ? "Continue sequential status polling until terminal status; at completed or completed_limited, retrieve certscore_get_scan_bundle before reporting the full scan results or final returned tally."
1211
+ : usable
1212
+ ? "The scan is terminal; retrieve certscore_get_scan_bundle before reporting the full scan results or final returned tally."
1213
+ : "The scan is terminal without a completed result; do not continue polling or report this preview as a final result."}`);
1134
1214
  return lines;
1135
1215
  }
1136
1216
  function boundedPreviewResultText(prefix, bodyLines, suffix) {
@@ -1203,6 +1283,9 @@ export function preConsentInventoryText(value) {
1203
1283
  return boundedResultText(`Pre-consent cookie/tracker evidence for ${domain}; scanId=${scanId}; ${returned} of ${total} rows returned${truncated ? " (truncated)" : ""}. Enumerate only these observed rows.`, rows.map((row) => preConsentRowText(compactPreConsentRow(row))), value);
1204
1284
  }
1205
1285
  export function pulseReportText(value, label = "CertScore report") {
1286
+ const noGoText = canonicalNoGoText(value);
1287
+ if (noGoText)
1288
+ return noGoText;
1206
1289
  const scanId = extractScanId(value) ?? "unknown";
1207
1290
  const domain = typeof value.domain === "string" ? value.domain : "unknown domain";
1208
1291
  const score = typeof value.summary?.score === "number"
@@ -1226,8 +1309,11 @@ export function markdownReportText(value) {
1226
1309
  return boundedResultText(`CertScore report for scanId=${extractScanId(value) ?? "unknown"}. The following is a bounded canonical report excerpt.`, excerpt ? [excerpt] : ["No Markdown report body was returned."], value);
1227
1310
  }
1228
1311
  export function scanBundleText(bundle) {
1312
+ const noGoText = canonicalNoGoText(bundle);
1313
+ if (noGoText)
1314
+ return noGoText;
1229
1315
  const score = typeof bundle.score === "number" ? `; CertScore score=${bundle.score}` : "";
1230
- const footer = [OBSERVATION_ONLY_DISCLAIMER, SCAN_BUNDLE_INTERPRETATION_STATEMENT];
1316
+ const footer = [SUCCESSFUL_BUNDLE_TRIAL_CTA, OBSERVATION_ONLY_DISCLAIMER, SCAN_BUNDLE_INTERPRETATION_STATEMENT];
1231
1317
  const lines = [
1232
1318
  SCAN_BUNDLE_RESPONSE_CONTRACT,
1233
1319
  `CertScore scan bundle for ${bundle.domain ?? "unknown domain"}; status=${bundle.status ?? "unknown"}${score}; scanId=${bundle.scanId ?? "unknown"}.`,
@@ -1251,6 +1337,9 @@ export function scanBundleText(bundle) {
1251
1337
  ? bundle.gpcResponse
1252
1338
  : null;
1253
1339
  if (gpcResponse) {
1340
+ const observationSummary = canonicalGpcObservationSummary(gpcResponse);
1341
+ if (observationSummary)
1342
+ append(`GPC observation: ${observationSummary}`);
1254
1343
  const proof = gpcResponse.comparison?.enabledProof;
1255
1344
  const californiaPolicy = gpcResponse.californiaPolicy;
1256
1345
  append(`GPC response: ${gpcResponse.findingTitle ?? "GPC response"}; status=${gpcResponse.status ?? "indeterminate"}; Sec-GPC: 1 proof retained on ${proof?.requestsWithSecGpc ?? 0} request(s).`);
@@ -1483,7 +1572,9 @@ export function buildScanBundle(input) {
1483
1572
  postAcceptObservation: input.scan.postAcceptObservation ?? null,
1484
1573
  postRefusalObservation: input.scan.postRefusalObservation ?? null,
1485
1574
  provenance: scanProvenance(input.scan, "existing_scan_retrieved"),
1486
- interpretationGuidance: interpretationGuidance(SCAN_BUNDLE_INTERPRETATION_STATEMENT),
1575
+ interpretationGuidance: interpretationGuidance(input.scan.gpcResponse?.contractVersion === "certscore.gpc-response-assessment.v3"
1576
+ ? SCAN_BUNDLE_INTERPRETATION_STATEMENT.replace("For gpcResponse,", "For the paired gpcResponse.status,") + " Report the separate bounded observation, CMP-recorded state and directly observed requests. Observation completion does not mean GPC was honored."
1577
+ : SCAN_BUNDLE_INTERPRETATION_STATEMENT),
1487
1578
  resultDisposition: input.scan.resultDisposition ?? null,
1488
1579
  noGo: input.scan.noGo ?? null,
1489
1580
  coverage: input.scan.coverage ?? null,
@@ -1516,7 +1607,7 @@ export function buildScanBundle(input) {
1516
1607
  ...(detail === "evidence" || detail === "full"
1517
1608
  ? { evidenceSummary: bundleEvidenceSummary(evidence, allFindings, links, detail === "full") }
1518
1609
  : {}),
1519
- ...(detail === "full" ? {
1610
+ ...(detail === "full" && Object.keys(report).length > 0 ? {
1520
1611
  fullReport: compactEvidenceValue(deduplicatedReport.residual, {
1521
1612
  arrayItems: 50,
1522
1613
  depth: 8,
@@ -1588,14 +1679,27 @@ export function buildScanBundle(input) {
1588
1679
  bundle.mcpMetadata.nextRecommendedMaxBytes = completeBytes <= responseCeilingBytes
1589
1680
  ? Math.max(5_000, Math.ceil(completeBytes / 1_000) * 1_000)
1590
1681
  : null;
1591
- bundle.recommendedNextAction = canonicalFindingsComplete
1682
+ bundle.recommendedNextAction = canonicalNoGo(bundle)?.recommendedNextAction ?? (canonicalFindingsComplete
1592
1683
  ? "Canonical findings complete; retry only for omitted envelope detail."
1593
1684
  : bundle.mcpMetadata.nextRecommendedMaxBytes
1594
1685
  ? `Retry with maxBytes=${bundle.mcpMetadata.nextRecommendedMaxBytes} to retrieve the complete requested tier, or open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`
1595
- : `The complete requested tier exceeds the MCP byte ceiling; open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`;
1686
+ : `The complete requested tier exceeds the MCP byte ceiling; open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`);
1596
1687
  refresh();
1597
1688
  };
1598
1689
  captureFullPayloadBytes();
1690
+ // Access disposition and its remedy outrank optional lane detail in a no-go envelope.
1691
+ // Retained evidence remains available through the reported content URLs.
1692
+ if (canonicalNoGo(bundle)) {
1693
+ for (const section of ["gpcResponse", "postAcceptObservation", "postRefusalObservation"]) {
1694
+ if (bundle.mcpMetadata.actualBytes <= maxBytes)
1695
+ break;
1696
+ if (bundle[section]) {
1697
+ markBudgetOmitted(section, "lane_detail_omitted_to_preserve_no_go");
1698
+ delete bundle[section];
1699
+ refresh();
1700
+ }
1701
+ }
1702
+ }
1599
1703
  if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.fullReport) {
1600
1704
  markBudgetOmitted("fullReport", "full_report_omitted_to_byte_limit");
1601
1705
  delete bundle.fullReport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certscore/mcp",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "mcpName": "ai.certscore/mcp",
5
5
  "private": false,
6
6
  "description": "MCP server for CertScore public website risk-signal workflows.",
@@ -69,6 +69,7 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@certscore/api-contracts": "workspace:*",
72
- "@certscore/sdk": "workspace:*"
72
+ "@certscore/sdk": "workspace:*",
73
+ "@website-signal-risk-scanner/shared": "workspace:*"
73
74
  }
74
75
  }
package/server-light.json CHANGED
@@ -24,7 +24,7 @@
24
24
  "theme": "dark"
25
25
  }
26
26
  ],
27
- "version": "0.2.19",
27
+ "version": "0.2.21",
28
28
  "remotes": [
29
29
  {
30
30
  "type": "streamable-http",
package/server.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "url": "https://github.com/ergoveritas1-alt/certscore.ai",
9
9
  "source": "github"
10
10
  },
11
- "version": "0.2.19",
11
+ "version": "0.2.21",
12
12
  "remotes": [
13
13
  {
14
14
  "type": "streamable-http",
@@ -20,7 +20,7 @@
20
20
  "registryType": "npm",
21
21
  "registryBaseUrl": "https://registry.npmjs.org",
22
22
  "identifier": "@certscore/mcp",
23
- "version": "0.2.19",
23
+ "version": "0.2.21",
24
24
  "transport": {
25
25
  "type": "stdio"
26
26
  },