@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/README.md +16 -3
- package/dist/certscore-mcp.mjs +20095 -15081
- 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 +12 -0
- package/dist/response-capture.d.ts.map +1 -0
- package/dist/response-capture.js +64 -0
- package/dist/server.d.ts +41 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +164 -34
- package/dist/tools.d.ts +11 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +157 -53
- package/package.json +3 -2
- package/server-light.json +1 -1
- package/server.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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 === "
|
|
101
|
-
const
|
|
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 =
|
|
106
|
-
?
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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: [
|
|
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 =
|
|
141
|
-
?
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1131
|
-
?
|
|
1132
|
-
:
|
|
1133
|
-
|
|
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(
|
|
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.
|
|
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
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.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.
|
|
23
|
+
"version": "0.2.21",
|
|
24
24
|
"transport": {
|
|
25
25
|
"type": "stdio"
|
|
26
26
|
},
|