@certscore/mcp 0.2.20 → 0.2.22
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/README.md +44 -8
- package/dist/adoption.d.ts +7 -0
- package/dist/adoption.d.ts.map +1 -0
- package/dist/adoption.js +23 -0
- package/dist/certscore-mcp.mjs +3927 -3252
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/response-capture.d.ts +15 -0
- package/dist/response-capture.d.ts.map +1 -0
- package/dist/response-capture.js +89 -0
- package/dist/response-guidance.d.ts +4 -0
- package/dist/response-guidance.d.ts.map +1 -0
- package/dist/response-guidance.js +100 -0
- package/dist/server.d.ts +18 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +182 -37
- package/dist/tools.d.ts +18 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +146 -58
- package/package.json +1 -1
- package/server-light.json +1 -1
- package/server.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { apiV2GpcResponseSchema, apiV2ChoicePathExecutionSchema } 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
|
+
}
|
|
8
|
+
function canonicalChoicePathExecutionText(observation) {
|
|
9
|
+
const parsed = apiV2ChoicePathExecutionSchema.safeParse(observation.execution);
|
|
10
|
+
if (!parsed.success)
|
|
11
|
+
return null;
|
|
12
|
+
const execution = parsed.data;
|
|
13
|
+
return `execution=${execution.status}; click completed=${execution.clickCompleted}; observation completed=${execution.observationCompleted}; consent confirmed=${execution.consentConfirmed}.`;
|
|
14
|
+
}
|
|
2
15
|
const MAX_ERROR_RESPONSE_BODY_CHARS = 2_000;
|
|
3
16
|
export const MAX_EVIDENCE_PACKET_CHARS = 250_000;
|
|
4
17
|
const EVIDENCE_STRING_CHARS = 4_000;
|
|
@@ -9,10 +22,13 @@ const LEGAL_REVIEW_DISCLAIMER = "CertScore results are automated public-web obse
|
|
|
9
22
|
const SCAN_PROVENANCE_GROUNDING = "retrievalMode describes how the current tool response obtained the scan; creationDecision describes whether the original scan request created or reused a scan only when that decision is retained. Never infer an unknown creationDecision from scan_id_lookup. For a reused or retrieved existing scan, use only persisted scanFrom and timestamps. Never infer its original scan region from the current request, the user's location, or a default execution region. If persisted region or timestamps are unavailable, report them as unavailable.";
|
|
10
23
|
const INTERPRETATION_STATEMENT = "The CertScore score covers observable public-web scan signals only. Do not infer technologies that are not listed in the returned evidence or any legal compliance status.";
|
|
11
24
|
const SCAN_BUNDLE_RESPONSE_CONTRACT = `Response contract: Report only observed CertScore evidence and CertScore classifications. criticality, priority, and confidence are CertScore metadata; regulatory review lenses are non-determinative CertScore review context—not legal severity, legal exposure, or a compliance determination. Absence of captured consent-action evidence does not establish what happens after Accept, Reject, or Decline. A confirmed post-action observation with termination.kind=evidence_satisfied means the observer intentionally stopped after retaining qualifying evidence; do not treat that termination as uncertainty about the returned observation. Keep any separately returned coverage limitation scoped to what was not measured. Do not extrapolate an observed embed, vendor, or request into unobserved cookies, fingerprinting, tracking, or processing, and do not infer violations or compliance beyond what CertScore observed. ${SCAN_PROVENANCE_GROUNDING}`;
|
|
25
|
+
const CHOICE_PATH_INTERPRETATION_STATEMENT = "Count execution.status succeeded and succeeded_with_confirmation as successful paths, with confirmation as a separate subset. A click alone is insufficient; registered paths may omit afterAction. Missing legacy execution remains unavailable. ";
|
|
12
26
|
const SCAN_BUNDLE_INTERPRETATION_STATEMENT = "Report only observed CertScore evidence and persisted CertScore classifications. For gpcResponse, use only GPC response, No observable GPC response, or indeterminate; do not call the result a GPC violation or say GPC was not honored. Keep its jurisdiction-neutral comparison separate from any explicitly returned California scoring policy. Without corresponding captured post-action evidence, do not infer what Accept, Reject, Decline, or another consent action would do; say the scan does not establish what happens after that action. When postAcceptObservation or postRefusalObservation is confirmed and termination.kind is evidence_satisfied, state the returned observation directly and explain that observation stopped intentionally after qualifying evidence was retained. Do not characterize that termination as uncertainty about the observation; mention unmeasured longer-term persistence only when relevant. Treat post-Accept activity as a score-neutral behavior baseline unless a separately projected finding says otherwise. Do not speculate that an observed embed, vendor, or request may cause additional cookies, fingerprinting, tracking, or processing unless CertScore observed that behavior. Treat returned priority or severity as a CertScore classification, not regulatory criticality or legal exposure; prefer ‘observed privacy risk signal’ or ‘CertScore finding’. Do not infer unobserved technologies, legal compliance, or a legal violation from scores or findings.";
|
|
13
27
|
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
28
|
const OBSERVATION_ONLY_DISCLAIMER = `${LEGAL_REVIEW_DISCLAIMER} No-go, not-observed, and limited-coverage results are not proof of compliance.`;
|
|
15
29
|
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.";
|
|
30
|
+
const PREVIEW_OBSERVATION_ONLY_DISCLAIMER = "Preliminary passive observations only; not findings, a score, or a final result.";
|
|
31
|
+
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; active workspace members receive self-serve scan access; Light remains no-auth.";
|
|
16
32
|
const MCP_SCAN_CREATION_POLL_DELAY_SECONDS = 15;
|
|
17
33
|
const MCP_QUEUED_POLL_DELAY_SECONDS = 10;
|
|
18
34
|
const MCP_RUNNING_POLL_DELAY_SECONDS = 5;
|
|
@@ -43,8 +59,9 @@ function toolResultSummary(payload) {
|
|
|
43
59
|
const previewSummary = preview?.summary && typeof preview.summary === "object" && !Array.isArray(preview.summary)
|
|
44
60
|
? preview.summary
|
|
45
61
|
: null;
|
|
62
|
+
const active = record.status === "queued" || record.status === "running" || record.status === "finalizing";
|
|
46
63
|
const preliminary = preview
|
|
47
|
-
? `; preliminary pre-consent preview=cookies ${previewSummary?.cookieCount ?? "unknown"}, trackers ${previewSummary?.trackerCount ?? "unknown"}, third-party requests ${previewSummary?.thirdPartyRequestCount ?? "unknown"}; preview is not final
|
|
64
|
+
? `; 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"}`
|
|
48
65
|
: "";
|
|
49
66
|
const recordLinks = record.links && typeof record.links === "object" && !Array.isArray(record.links)
|
|
50
67
|
? record.links
|
|
@@ -71,7 +88,7 @@ export function toToolResult(payload, text) {
|
|
|
71
88
|
const structuredContent = payload !== null && typeof payload === "object" && !Array.isArray(payload)
|
|
72
89
|
? payload
|
|
73
90
|
: { value: payload };
|
|
74
|
-
return {
|
|
91
|
+
return transferResponseCapture(payload, {
|
|
75
92
|
structuredContent,
|
|
76
93
|
content: [
|
|
77
94
|
{
|
|
@@ -79,9 +96,9 @@ export function toToolResult(payload, text) {
|
|
|
79
96
|
text: text ?? toolResultSummary(payload)
|
|
80
97
|
}
|
|
81
98
|
]
|
|
82
|
-
};
|
|
99
|
+
});
|
|
83
100
|
}
|
|
84
|
-
export function toToolError(error) {
|
|
101
|
+
export function toToolError(error, context = {}) {
|
|
85
102
|
const responseRecord = error instanceof CertScoreError && error.responseBody && typeof error.responseBody === "object" && !Array.isArray(error.responseBody)
|
|
86
103
|
? error.responseBody
|
|
87
104
|
: null;
|
|
@@ -91,7 +108,7 @@ export function toToolError(error) {
|
|
|
91
108
|
const status = error instanceof CertScoreError ? error.status : undefined;
|
|
92
109
|
const retryable = typeof terminalError?.retryable === "boolean"
|
|
93
110
|
? terminalError.retryable
|
|
94
|
-
: status === 429 || (typeof status === "number" && status >= 500);
|
|
111
|
+
: (error instanceof Error && error.name === "TimeoutError") || status === 429 || (typeof status === "number" && status >= 500);
|
|
95
112
|
const retryAfterSeconds = typeof terminalError?.retryAfterSeconds === "number"
|
|
96
113
|
? terminalError.retryAfterSeconds
|
|
97
114
|
: error instanceof CertScoreError && "retryAfterSeconds" in error && typeof error.retryAfterSeconds === "number"
|
|
@@ -100,28 +117,39 @@ export function toToolError(error) {
|
|
|
100
117
|
? 30
|
|
101
118
|
: null;
|
|
102
119
|
const code = error instanceof CertScoreError ? error.code : "internal_error";
|
|
103
|
-
const reasonCode = terminalError?.reasonCode === "
|
|
104
|
-
const
|
|
120
|
+
const reasonCode = typeof terminalError?.reasonCode === "string" && ["non_public_target", "domain_not_found", "dns_unavailable"].includes(terminalError.reasonCode) ? terminalError.reasonCode : null;
|
|
121
|
+
const targetRejected = context.scanCreation === true && code === "invalid_url" && status === 400;
|
|
122
|
+
const originalMessage = error instanceof Error ? error.message : "Unknown CertScore MCP error.";
|
|
123
|
+
const message = targetRejected ? `Scan target rejected. No scan was started. ${originalMessage}` : originalMessage;
|
|
105
124
|
const creationRateLimit = terminalError?.creationRateLimit && typeof terminalError.creationRateLimit === "object" && !Array.isArray(terminalError.creationRateLimit)
|
|
106
125
|
? terminalError.creationRateLimit
|
|
107
126
|
: null;
|
|
108
|
-
const recommendedNextAction =
|
|
109
|
-
?
|
|
127
|
+
const recommendedNextAction = targetRejected
|
|
128
|
+
? reasonCode === "non_public_target"
|
|
129
|
+
? "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."
|
|
130
|
+
: '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.'
|
|
110
131
|
: creationRateLimit
|
|
111
|
-
? `No scan was created. Wait ${retryAfterSeconds ?? 30} seconds,
|
|
112
|
-
:
|
|
113
|
-
?
|
|
114
|
-
:
|
|
132
|
+
? `No scan was created. Wait at least ${retryAfterSeconds ?? 30} seconds before retrying. To answer now, use certscore_get_latest_domain_scan if an existing scan meets the user's needs. Do not reconnect or create duplicate requests to bypass a quota.`
|
|
133
|
+
: typeof terminalError?.recommendedNextAction === "string"
|
|
134
|
+
? terminalError.recommendedNextAction
|
|
135
|
+
: retryable
|
|
136
|
+
? `Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. Stop and contact support@certscore.ai if the error repeats.`
|
|
137
|
+
: "Correct the request using the error details, then retry only if the requested operation is still appropriate.";
|
|
115
138
|
const payload = {
|
|
116
139
|
error: {
|
|
117
140
|
code,
|
|
118
141
|
reasonCode,
|
|
119
142
|
message,
|
|
120
|
-
retryable,
|
|
121
|
-
retryAfterSeconds,
|
|
143
|
+
retryable: targetRejected ? false : retryable,
|
|
144
|
+
retryAfterSeconds: targetRejected ? null : retryAfterSeconds,
|
|
122
145
|
recommendedNextAction,
|
|
146
|
+
...((status === 403 || status === 429) ? { upgradeSupportEmail: "support@certscore.ai" } : {}),
|
|
147
|
+
...(context.scanCreation && status === 403 ? { scanStarted: false, alternativeTool: "certscore_get_latest_domain_scan" } : {}),
|
|
148
|
+
...(targetRejected ? { field: "url", scanStarted: false, inputCorrectionRequired: true } : {}),
|
|
123
149
|
...(creationRateLimit
|
|
124
|
-
? { creationRateLimit
|
|
150
|
+
? { creationRateLimit, scanStarted: false, quotaConsumed: false,
|
|
151
|
+
alternativeTool: "certscore_get_latest_domain_scan",
|
|
152
|
+
recovery: { action: "wait_for_quota", retryAfterSeconds, requiresReauthorization: false } }
|
|
125
153
|
: {}),
|
|
126
154
|
...(error instanceof CertScoreError ? {
|
|
127
155
|
name: error.name,
|
|
@@ -130,26 +158,31 @@ export function toToolError(error) {
|
|
|
130
158
|
} : {})
|
|
131
159
|
}
|
|
132
160
|
};
|
|
133
|
-
return {
|
|
134
|
-
content: [
|
|
161
|
+
return withResponseCapture({
|
|
162
|
+
content: [
|
|
163
|
+
{ type: "text", text: JSON.stringify(payload) },
|
|
164
|
+
...(targetRejected ? [{ type: "text", text: `${message} ${recommendedNextAction}` }] : []),
|
|
165
|
+
],
|
|
135
166
|
isError: true
|
|
136
|
-
};
|
|
167
|
+
}, { ...(targetRejected || typeof terminalError?.recommendedNextAction !== "string" ? { recommendedNextAction } : {}), ...(getCertScoreErrorContext(error) ? { upstream: getCertScoreErrorContext(error) } : {}) });
|
|
137
168
|
}
|
|
138
|
-
export function toInvalidArgumentsToolError(errorMessage) {
|
|
139
|
-
const tool = errorMessage.match(/tool ([a-z_]+)/i)?.[1] ?? null;
|
|
140
|
-
const field = errorMessage.match(/\bat ([a-zA-Z0-9_.-]+)/)?.[1]
|
|
169
|
+
export function toInvalidArgumentsToolError(errorMessage, validation) {
|
|
170
|
+
const tool = validation?.tool ?? errorMessage.match(/tool ([a-z_]+)/i)?.[1] ?? null;
|
|
171
|
+
const field = validation?.issues[0]?.field ?? errorMessage.match(/\bat ([a-zA-Z0-9_.-]+)/)?.[1]
|
|
141
172
|
?? (errorMessage.includes("url") ? "url" : errorMessage.includes("scanId") ? "scanId" : null);
|
|
142
|
-
const missing = /required|expected string, received undefined/i.test(errorMessage);
|
|
143
|
-
const message =
|
|
144
|
-
?
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
173
|
+
const missing = validation?.issues[0]?.required === true || /required|expected string, received undefined/i.test(errorMessage);
|
|
174
|
+
const message = validation && validation.issues.length > 1
|
|
175
|
+
? `Invalid arguments in fields: ${validation.issues.map(issue => issue.field).join(", ")}.`
|
|
176
|
+
: field
|
|
177
|
+
? missing
|
|
178
|
+
? `The ${field} field is required.`
|
|
179
|
+
: `The ${field} field is invalid.`
|
|
180
|
+
: `The arguments for ${tool ?? "this tool"} are invalid.`;
|
|
148
181
|
const recommendedNextAction = field === "url"
|
|
149
182
|
? "Provide a public URL or domain."
|
|
150
183
|
: field === "scanId"
|
|
151
184
|
? "Provide the stable scanId returned by certscore_scan_site."
|
|
152
|
-
: "Correct the named fields using the tool input schema, then retry.";
|
|
185
|
+
: "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.";
|
|
153
186
|
const error = {
|
|
154
187
|
code: "invalid_arguments",
|
|
155
188
|
message,
|
|
@@ -157,7 +190,8 @@ export function toInvalidArgumentsToolError(errorMessage) {
|
|
|
157
190
|
retryable: false,
|
|
158
191
|
retryAfterSeconds: null,
|
|
159
192
|
recommendedNextAction,
|
|
160
|
-
mcpCode: -32602
|
|
193
|
+
mcpCode: -32602,
|
|
194
|
+
...(validation ? { issues: validation.issues } : {})
|
|
161
195
|
};
|
|
162
196
|
const payload = tool === "certscore_scan_site"
|
|
163
197
|
? {
|
|
@@ -172,28 +206,31 @@ export function toInvalidArgumentsToolError(errorMessage) {
|
|
|
172
206
|
observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
|
|
173
207
|
}
|
|
174
208
|
: { error };
|
|
175
|
-
return {
|
|
209
|
+
return withResponseCapture({
|
|
176
210
|
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
177
211
|
...(tool === "certscore_scan_site" ? { structuredContent: payload } : {}),
|
|
178
212
|
isError: true
|
|
179
|
-
};
|
|
213
|
+
}, { message: error.message, recommendedNextAction: error.recommendedNextAction });
|
|
180
214
|
}
|
|
181
215
|
export function toInvalidScanIdToolError() {
|
|
182
216
|
const error = {
|
|
183
217
|
code: "invalid_scan_id",
|
|
184
218
|
field: "scanId",
|
|
185
|
-
message: "
|
|
219
|
+
message: "Invalid scanId. Use the unchanged UUID returned by certscore_scan_site. This request did not start a scan.",
|
|
186
220
|
retryable: false,
|
|
187
221
|
retryAfterSeconds: null,
|
|
188
222
|
recommendedNextAction: "Use the unchanged scanId returned by certscore_scan_site. Do not use placeholders, report URLs, domains, or job IDs.",
|
|
189
223
|
mcpCode: -32602
|
|
190
224
|
};
|
|
191
|
-
return {
|
|
225
|
+
return withResponseCapture({
|
|
192
226
|
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
193
227
|
isError: true
|
|
194
|
-
};
|
|
228
|
+
}, { message: error.message, recommendedNextAction: error.recommendedNextAction });
|
|
195
229
|
}
|
|
196
230
|
function terminalErrorForResult(value) {
|
|
231
|
+
const terminalGuidance = value.status === "failed" || value.status === "expired"
|
|
232
|
+
? ` 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}.` : ""}`
|
|
233
|
+
: "";
|
|
197
234
|
const existing = value.error && typeof value.error === "object" && !Array.isArray(value.error)
|
|
198
235
|
? value.error
|
|
199
236
|
: null;
|
|
@@ -204,11 +241,11 @@ function terminalErrorForResult(value) {
|
|
|
204
241
|
message: typeof existing.message === "string" ? existing.message : "The scan did not produce a canonical result.",
|
|
205
242
|
retryable,
|
|
206
243
|
retryAfterSeconds: typeof existing.retryAfterSeconds === "number" ? existing.retryAfterSeconds : retryable ? 30 : null,
|
|
207
|
-
recommendedNextAction: typeof existing.recommendedNextAction === "string"
|
|
244
|
+
recommendedNextAction: (typeof existing.recommendedNextAction === "string"
|
|
208
245
|
? existing.recommendedNextAction
|
|
209
246
|
: retryable
|
|
210
247
|
? "Wait for the recommended delay, then retry certscore_scan_site with freshness=refresh."
|
|
211
|
-
: "Stop and review the scan limitations before deciding whether to change the URL."
|
|
248
|
+
: "Stop and review the scan limitations before deciding whether to change the URL.") + (typeof existing.recommendedNextAction === "string" && existing.recommendedNextAction.endsWith(terminalGuidance) ? "" : terminalGuidance)
|
|
212
249
|
};
|
|
213
250
|
}
|
|
214
251
|
if (value.status === "completed_limited" && value.noGo) {
|
|
@@ -246,7 +283,8 @@ function terminalErrorForResult(value) {
|
|
|
246
283
|
recommendedNextAction: "Wait for the recommended delay, then retry the same certscore_scan_site request."
|
|
247
284
|
}
|
|
248
285
|
};
|
|
249
|
-
|
|
286
|
+
const error = fallback[String(value.status)];
|
|
287
|
+
return error ? { ...error, recommendedNextAction: error.recommendedNextAction + terminalGuidance } : null;
|
|
250
288
|
}
|
|
251
289
|
function scanProvenance(value, fallbackMode) {
|
|
252
290
|
const executionMode = value.executionMode === "new_scan" || value.executionMode === "reused_scan"
|
|
@@ -311,6 +349,12 @@ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown",
|
|
|
311
349
|
const hasPreConsentPreview = Boolean(value.preConsentPreview &&
|
|
312
350
|
typeof value.preConsentPreview === "object" &&
|
|
313
351
|
!Array.isArray(value.preConsentPreview));
|
|
352
|
+
const preConsentPreview = hasPreConsentPreview
|
|
353
|
+
? {
|
|
354
|
+
...value.preConsentPreview,
|
|
355
|
+
observationOnlyDisclaimer: PREVIEW_OBSERVATION_ONLY_DISCLAIMER,
|
|
356
|
+
}
|
|
357
|
+
: value.preConsentPreview;
|
|
314
358
|
const reportUrl = usable
|
|
315
359
|
? typeof value.reportUrl === "string" && value.reportUrl.trim()
|
|
316
360
|
? value.reportUrl.trim()
|
|
@@ -325,8 +369,9 @@ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown",
|
|
|
325
369
|
: null;
|
|
326
370
|
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}.`;
|
|
327
371
|
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.`;
|
|
328
|
-
return {
|
|
372
|
+
return withResponseCapture({
|
|
329
373
|
...value,
|
|
374
|
+
preConsentPreview,
|
|
330
375
|
retryAfterSeconds,
|
|
331
376
|
error,
|
|
332
377
|
reportUrl,
|
|
@@ -340,14 +385,14 @@ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown",
|
|
|
340
385
|
? `Call certscore_get_scan_bundle with scanId ${stableScanId ?? value.jobId} for the completed scan's final returned tally, canonical findings, and limitations.`
|
|
341
386
|
: "Review the result and retained limitations.")),
|
|
342
387
|
observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
|
|
343
|
-
};
|
|
388
|
+
}, error && !value.error && !value.noGo ? { message: error.message, recommendedNextAction: error.recommendedNextAction } : {});
|
|
344
389
|
}
|
|
345
390
|
export function withMcpScanProvenanceGuidance(value, fallbackProvenanceMode) {
|
|
346
391
|
const guided = withMcpAgentGuidance(value, fallbackProvenanceMode, "scan_status");
|
|
347
|
-
return {
|
|
392
|
+
return transferResponseCapture(guided, {
|
|
348
393
|
...guided,
|
|
349
394
|
interpretationGuidance: interpretationGuidance(`${INTERPRETATION_STATEMENT} ${SCAN_PROVENANCE_GROUNDING}`)
|
|
350
|
-
};
|
|
395
|
+
});
|
|
351
396
|
}
|
|
352
397
|
export function boundEvidencePacket(payload, maxSerializedChars = MAX_EVIDENCE_PACKET_CHARS) {
|
|
353
398
|
const originalSerializedChars = measureSerializedChars(payload);
|
|
@@ -762,6 +807,10 @@ export function findingsFromReport(report) {
|
|
|
762
807
|
export function exportFindings(report) {
|
|
763
808
|
return {
|
|
764
809
|
type: "certscore_mcp_findings_export",
|
|
810
|
+
exportVersion: "certscore.findings-export.v1",
|
|
811
|
+
exportedAt: new Date().toISOString(),
|
|
812
|
+
returnedFindingCount: findingsFromReport(report).length,
|
|
813
|
+
completeness: "canonical findings returned by the report; not a complete inventory of website behavior",
|
|
765
814
|
scanId: scanIdFromPulse(report),
|
|
766
815
|
domain: report.domain ?? report.request?.domain ?? null,
|
|
767
816
|
summary: report.summary ?? null,
|
|
@@ -940,7 +989,11 @@ function findingText(finding, priorityLabel = "criticality") {
|
|
|
940
989
|
const nextStep = typeof finding.nextStep === "string" && finding.nextStep.trim()
|
|
941
990
|
? `; canonical next step=${boundedText(finding.nextStep.trim(), 180)}`
|
|
942
991
|
: "";
|
|
943
|
-
|
|
992
|
+
const identity = typeof finding.id === "string" ? `; findingId=${finding.id}` : "";
|
|
993
|
+
if (finding.plainEnglish == null && evidence.summary == null) {
|
|
994
|
+
return `- ${finding.label ?? finding.id ?? "Projected finding"}${identity}; ${priorityLabel}=${finding.criticality ?? "unknown"}; confidence=${finding.confidence ?? "unknown"}; description and evidence detail are not included in this response tier.`;
|
|
995
|
+
}
|
|
996
|
+
return `- ${finding.label ?? finding.id ?? "Projected finding"}${identity}; ${priorityLabel}=${finding.criticality ?? "unknown"}; confidence=${finding.confidence ?? "unknown"}; observation=${finding.plainEnglish ?? evidence.summary ?? "No compact description available"}; evidence=${evidence.basis ?? "unknown"}/${evidence.phase ?? "phase unknown"}: ${evidence.summary ?? "No compact evidence summary available"}; review lenses=${lenses}${nextStep}.`;
|
|
944
997
|
}
|
|
945
998
|
function executiveOverviewText(summary) {
|
|
946
999
|
if (!summary)
|
|
@@ -1022,7 +1075,9 @@ export function scanStatusText(value) {
|
|
|
1022
1075
|
const noGoText = canonicalNoGoText(value);
|
|
1023
1076
|
if (noGoText)
|
|
1024
1077
|
return noGoText;
|
|
1025
|
-
const reportUrl =
|
|
1078
|
+
const reportUrl = value.status === "completed" || value.status === "completed_limited"
|
|
1079
|
+
? reportUrlFor(value)
|
|
1080
|
+
: null;
|
|
1026
1081
|
const nextAction = typeof value.recommendedNextAction === "string" && value.recommendedNextAction.trim()
|
|
1027
1082
|
? value.recommendedNextAction.trim()
|
|
1028
1083
|
: "Review the returned status and retained limitations.";
|
|
@@ -1064,6 +1119,9 @@ function terminalLaneResultTextLines(value) {
|
|
|
1064
1119
|
? value.gpcResponse
|
|
1065
1120
|
: null;
|
|
1066
1121
|
if (gpcResponse) {
|
|
1122
|
+
const observationSummary = canonicalGpcObservationSummary(gpcResponse);
|
|
1123
|
+
if (observationSummary)
|
|
1124
|
+
lines.push(`GPC observation: ${observationSummary}`);
|
|
1067
1125
|
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).`);
|
|
1068
1126
|
}
|
|
1069
1127
|
for (const [field, label] of [["postAcceptObservation", "Accept Path"], ["postRefusalObservation", "Reject Path"]]) {
|
|
@@ -1071,6 +1129,9 @@ function terminalLaneResultTextLines(value) {
|
|
|
1071
1129
|
? value[field]
|
|
1072
1130
|
: null;
|
|
1073
1131
|
if (observation && typeof observation.interpretation === "string") {
|
|
1132
|
+
const execution = canonicalChoicePathExecutionText(observation);
|
|
1133
|
+
if (execution)
|
|
1134
|
+
lines.push(`${label} execution: ${execution}`);
|
|
1074
1135
|
lines.push(`${label}: ${observation.interpretation}`);
|
|
1075
1136
|
}
|
|
1076
1137
|
}
|
|
@@ -1110,9 +1171,17 @@ function preConsentPreviewTextLines(value) {
|
|
|
1110
1171
|
const returnedTrackingVendorCount = summary.returnedTrackingVendorCount ?? trackers.length;
|
|
1111
1172
|
const capturedOperationalVendorCount = summary.operationalVendorCount ?? operationalVendors.length;
|
|
1112
1173
|
const returnedOperationalVendorCount = summary.returnedOperationalVendorCount ?? operationalVendors.length;
|
|
1174
|
+
const status = String(value.status ?? "");
|
|
1175
|
+
const active = status === "queued" || status === "running" || status === "finalizing";
|
|
1176
|
+
const usable = status === "completed" || status === "completed_limited";
|
|
1177
|
+
const previewWorkflowGuidance = active
|
|
1178
|
+
? "Wait for terminal scan status, then call certscore_get_scan_bundle for the completed scan's final returned tally, canonical findings, and coverage limitations."
|
|
1179
|
+
: usable
|
|
1180
|
+
? "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."
|
|
1181
|
+
: "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.";
|
|
1113
1182
|
const lines = [
|
|
1114
1183
|
`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"}.`,
|
|
1115
|
-
|
|
1184
|
+
`PARTIAL PREVIEW: These are checkpoint-only partial counts, not the full scan tally. ${previewWorkflowGuidance}`,
|
|
1116
1185
|
"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.",
|
|
1117
1186
|
];
|
|
1118
1187
|
if (cookies.length > 0) {
|
|
@@ -1160,10 +1229,11 @@ function preConsentPreviewTextLines(value) {
|
|
|
1160
1229
|
String(capturedOperationalVendorCount) !== String(returnedOperationalVendorCount)) {
|
|
1161
1230
|
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.`);
|
|
1162
1231
|
}
|
|
1163
|
-
|
|
1164
|
-
?
|
|
1165
|
-
:
|
|
1166
|
-
|
|
1232
|
+
lines.push(`${PREVIEW_OBSERVATION_ONLY_DISCLAIMER} ${active
|
|
1233
|
+
? "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."
|
|
1234
|
+
: usable
|
|
1235
|
+
? "The scan is terminal; retrieve certscore_get_scan_bundle before reporting the full scan results or final returned tally."
|
|
1236
|
+
: "The scan is terminal without a completed result; do not continue polling or report this preview as a final result."}`);
|
|
1167
1237
|
return lines;
|
|
1168
1238
|
}
|
|
1169
1239
|
function boundedPreviewResultText(prefix, bodyLines, suffix) {
|
|
@@ -1251,7 +1321,10 @@ export function pulseReportText(value, label = "CertScore report") {
|
|
|
1251
1321
|
const body = [
|
|
1252
1322
|
...(overview ? [overview] : []),
|
|
1253
1323
|
`Canonical projected findings returned in this ${label.toLocaleLowerCase()}: ${findings.length}.`,
|
|
1254
|
-
...findings.map((finding) => findingText(finding))
|
|
1324
|
+
...findings.map((finding) => findingText(finding)),
|
|
1325
|
+
...(findings.some((finding) => finding.plainEnglish == null && finding.evidence?.summary == null)
|
|
1326
|
+
? [`For finding descriptions and evidence, call certscore_list_findings with scanId=${scanId}, or certscore_explain_finding with that scanId and a returned findingId. No new scan is needed.`]
|
|
1327
|
+
: [])
|
|
1255
1328
|
];
|
|
1256
1329
|
return boundedResultText(`${label} for ${domain}; scanId=${scanId}${score === null ? "" : `; CertScore score=${score}`}.`, body, value);
|
|
1257
1330
|
}
|
|
@@ -1261,15 +1334,19 @@ export function markdownReportText(value) {
|
|
|
1261
1334
|
const excerpt = markdown.length > excerptLimit ? `${markdown.slice(0, excerptLimit)}\n…[markdown truncated for MCP TextContent]` : markdown;
|
|
1262
1335
|
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);
|
|
1263
1336
|
}
|
|
1264
|
-
export function scanBundleText(bundle) {
|
|
1337
|
+
export function scanBundleText(bundle, options = {}) {
|
|
1265
1338
|
const noGoText = canonicalNoGoText(bundle);
|
|
1266
1339
|
if (noGoText)
|
|
1267
1340
|
return noGoText;
|
|
1268
1341
|
const score = typeof bundle.score === "number" ? `; CertScore score=${bundle.score}` : "";
|
|
1269
|
-
const footer = [OBSERVATION_ONLY_DISCLAIMER, SCAN_BUNDLE_INTERPRETATION_STATEMENT];
|
|
1342
|
+
const footer = [...(options.lightTrialCta ? [SUCCESSFUL_BUNDLE_TRIAL_CTA] : []), OBSERVATION_ONLY_DISCLAIMER, SCAN_BUNDLE_INTERPRETATION_STATEMENT];
|
|
1270
1343
|
const lines = [
|
|
1271
1344
|
SCAN_BUNDLE_RESPONSE_CONTRACT,
|
|
1272
1345
|
`CertScore scan bundle for ${bundle.domain ?? "unknown domain"}; status=${bundle.status ?? "unknown"}${score}; scanId=${bundle.scanId ?? "unknown"}.`,
|
|
1346
|
+
`Risk: ${bundle.riskLevel ?? "unknown"}. Finding IDs (returned): ${Array.isArray(bundle.findings) ? bundle.findings.slice(0, 20).map((finding) => String(finding.id ?? "unknown").slice(0, 120)).join(", ") || "none" : "unavailable"}.`,
|
|
1347
|
+
bundle.preConsentCookiesTrackers
|
|
1348
|
+
? `Pre-consent inventory: total=${bundle.preConsentCookiesTrackers.total ?? "unknown"}; returned=${bundle.preConsentCookiesTrackers.rows?.length ?? "unknown"}. Counts describe retained coverage, not consent compliance.`
|
|
1349
|
+
: `Pre-consent inventory: ${bundle.mcpMetadata?.omittedSections?.includes("preConsentCookiesTrackers") ? "omitted to fit the response byte limit" : "not included in this response"}. Call certscore_get_pre_consent_cookies_trackers with scanId=${bundle.scanId ?? "unknown"} for retained rows and counts; no new scan is needed.`,
|
|
1273
1350
|
canonicalScanProvenanceText(bundle),
|
|
1274
1351
|
`Full report: ${bundle.reportUrl ?? (bundle.scanId ? `https://certscore.ai/scan/${encodeURIComponent(String(bundle.scanId))}` : "not available")}.`
|
|
1275
1352
|
];
|
|
@@ -1290,6 +1367,9 @@ export function scanBundleText(bundle) {
|
|
|
1290
1367
|
? bundle.gpcResponse
|
|
1291
1368
|
: null;
|
|
1292
1369
|
if (gpcResponse) {
|
|
1370
|
+
const observationSummary = canonicalGpcObservationSummary(gpcResponse);
|
|
1371
|
+
if (observationSummary)
|
|
1372
|
+
append(`GPC observation: ${observationSummary}`);
|
|
1293
1373
|
const proof = gpcResponse.comparison?.enabledProof;
|
|
1294
1374
|
const californiaPolicy = gpcResponse.californiaPolicy;
|
|
1295
1375
|
append(`GPC response: ${gpcResponse.findingTitle ?? "GPC response"}; status=${gpcResponse.status ?? "indeterminate"}; Sec-GPC: 1 proof retained on ${proof?.requestsWithSecGpc ?? 0} request(s).`);
|
|
@@ -1307,6 +1387,9 @@ export function scanBundleText(bundle) {
|
|
|
1307
1387
|
const intentionalEvidenceStop = termination?.kind === "evidence_satisfied" && termination.intentional === true
|
|
1308
1388
|
? " The observation then stopped intentionally because qualifying evidence had been captured."
|
|
1309
1389
|
: "";
|
|
1390
|
+
const execution = canonicalChoicePathExecutionText(postAccept);
|
|
1391
|
+
if (execution)
|
|
1392
|
+
append(`Accept Path execution: ${execution}`);
|
|
1310
1393
|
append(`Accept Path: ${postAccept.interpretation}${intentionalEvidenceStop}`);
|
|
1311
1394
|
for (const limitation of Array.isArray(postAccept.coverageLimitations)
|
|
1312
1395
|
? postAccept.coverageLimitations.slice(0, 3)
|
|
@@ -1324,6 +1407,9 @@ export function scanBundleText(bundle) {
|
|
|
1324
1407
|
const intentionalEvidenceStop = termination?.kind === "evidence_satisfied" && termination.intentional === true
|
|
1325
1408
|
? " The observation then stopped intentionally because qualifying evidence had been captured."
|
|
1326
1409
|
: "";
|
|
1410
|
+
const execution = canonicalChoicePathExecutionText(postRefusal);
|
|
1411
|
+
if (execution)
|
|
1412
|
+
append(`Reject Path execution: ${execution}`);
|
|
1327
1413
|
append(`Reject Path: ${postRefusal.interpretation}${intentionalEvidenceStop}`);
|
|
1328
1414
|
for (const limitation of Array.isArray(postRefusal.coverageLimitations)
|
|
1329
1415
|
? postRefusal.coverageLimitations.slice(0, 3)
|
|
@@ -1388,9 +1474,6 @@ export function scanBundleText(bundle) {
|
|
|
1388
1474
|
append(`${rows.length - rowsRendered} additional returned pre-consent row${rows.length - rowsRendered === 1 ? " was" : "s were"} omitted from TextContent to preserve the size limit; see structuredContent or the report URL.`);
|
|
1389
1475
|
}
|
|
1390
1476
|
}
|
|
1391
|
-
else {
|
|
1392
|
-
append("No row-level pre-consent inventory was available for this result; review coverage and limitations before interpreting absence.");
|
|
1393
|
-
}
|
|
1394
1477
|
lines.push(...footer);
|
|
1395
1478
|
return lines.join("\n");
|
|
1396
1479
|
}
|
|
@@ -1522,7 +1605,10 @@ export function buildScanBundle(input) {
|
|
|
1522
1605
|
postAcceptObservation: input.scan.postAcceptObservation ?? null,
|
|
1523
1606
|
postRefusalObservation: input.scan.postRefusalObservation ?? null,
|
|
1524
1607
|
provenance: scanProvenance(input.scan, "existing_scan_retrieved"),
|
|
1525
|
-
interpretationGuidance: interpretationGuidance(
|
|
1608
|
+
interpretationGuidance: interpretationGuidance((input.scan.postAcceptObservation || input.scan.postRefusalObservation ? CHOICE_PATH_INTERPRETATION_STATEMENT : "") +
|
|
1609
|
+
(input.scan.gpcResponse?.contractVersion === "certscore.gpc-response-assessment.v3"
|
|
1610
|
+
? 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."
|
|
1611
|
+
: SCAN_BUNDLE_INTERPRETATION_STATEMENT)),
|
|
1526
1612
|
resultDisposition: input.scan.resultDisposition ?? null,
|
|
1527
1613
|
noGo: input.scan.noGo ?? null,
|
|
1528
1614
|
coverage: input.scan.coverage ?? null,
|
|
@@ -1703,7 +1789,9 @@ export function buildScanBundle(input) {
|
|
|
1703
1789
|
}
|
|
1704
1790
|
if (bundle.mcpMetadata.actualBytes > maxBytes) {
|
|
1705
1791
|
markBudgetOmitted("duplicateGuidance", "guidance_compacted_to_preserve_priority_content");
|
|
1706
|
-
bundle.interpretationGuidance = interpretationGuidance(
|
|
1792
|
+
bundle.interpretationGuidance = interpretationGuidance((bundle.postAcceptObservation || bundle.postRefusalObservation
|
|
1793
|
+
? "Both execution success statuses count; confirmation is separate. Missing execution is unavailable. " : "") +
|
|
1794
|
+
COMPACT_SCAN_BUNDLE_INTERPRETATION_STATEMENT);
|
|
1707
1795
|
bundle.observationOnlyDisclaimer = COMPACT_OBSERVATION_ONLY_DISCLAIMER;
|
|
1708
1796
|
bundle.disclaimer = null;
|
|
1709
1797
|
refresh();
|
package/package.json
CHANGED
package/server-light.json
CHANGED
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.
|
|
11
|
+
"version": "0.2.22",
|
|
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.
|
|
23
|
+
"version": "0.2.22",
|
|
24
24
|
"transport": {
|
|
25
25
|
"type": "stdio"
|
|
26
26
|
},
|