@certscore/mcp 0.2.15 → 0.2.17
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 +82 -52
- package/dist/certscore-mcp.mjs +4193 -4166
- package/dist/index.js +1 -1
- package/dist/server.d.ts +33 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +374 -97
- package/dist/tools.d.ts +25 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +1074 -85
- package/package.json +1 -1
- package/server-light.json +19 -4
- package/server.json +2 -2
package/dist/server.js
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
|
-
import { CertScoreClient
|
|
2
|
-
import { certScoreMcpToolContracts } from "@certscore/api-contracts";
|
|
1
|
+
import { CertScoreClient } from "@certscore/sdk";
|
|
2
|
+
import { certScoreMcpToolContracts, isCanonicalScanId } from "@certscore/api-contracts";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { CERTSCORE_MCP_VERSION } from "./version.js";
|
|
5
|
-
import { boundEvidencePacket, buildScanBundle, exportFindings, limitPreConsentRows, normalizeDetail, normalizeFormat, paginateFindingList, toInvalidArgumentsToolError, toToolError, toToolResult, withMcpAgentGuidance } from "./tools.js";
|
|
6
|
-
const
|
|
5
|
+
import { boundEvidencePacket, buildScanBundle, exportFindings, findingListText, limitPreConsentRows, markdownReportText, MAX_EVIDENCE_PACKET_CHARS, normalizeDetail, normalizeFormat, paginateFindingList, preConsentInventoryText, pulseReportText, scanBundleText, scanSiteText, scanStatusText, toInvalidArgumentsToolError, toInvalidScanIdToolError, toToolError, toToolResult, withMcpAgentGuidance, withMcpScanProvenanceGuidance } from "./tools.js";
|
|
6
|
+
const LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES = 25_000;
|
|
7
|
+
const MCP_SCAN_SITE_TARGET_RESPONSE_MS = 11_000;
|
|
8
|
+
const MCP_SCAN_SITE_TARGET_RESPONSE_RESERVE_MS = 250;
|
|
9
|
+
const MCP_SCAN_SITE_PREVIEW_POLL_INTERVAL_MS = 750;
|
|
10
|
+
const MCP_SCAN_SITE_MAX_PREVIEW_WAIT_MS = 10_000;
|
|
11
|
+
function exampleDomainDemoSubstitution(requestedUrl, demoUrl) {
|
|
12
|
+
if (!demoUrl)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(requestedUrl.includes("://") ? requestedUrl : `https://${requestedUrl}`);
|
|
16
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
17
|
+
const reserved = ["example.com", "example.net", "example.org"].some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
18
|
+
if (!reserved)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
requestedUrl,
|
|
22
|
+
effectiveUrl: demoUrl,
|
|
23
|
+
reason: "iana_example_domain",
|
|
24
|
+
message: "The requested IANA example domain is a documentation placeholder, so CertScore scanned its controlled demonstration site instead. Findings describe the effective URL, not the requested placeholder."
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function withExampleDomainDemo(value, substitution) {
|
|
32
|
+
return substitution ? { ...value, demoSubstitution: substitution } : value;
|
|
33
|
+
}
|
|
34
|
+
function exampleDomainDemoText(value, substitution) {
|
|
35
|
+
return scanSiteText(value, substitution
|
|
36
|
+
? [`${substitution.message} Substitution provenance is in structuredContent.`]
|
|
37
|
+
: []);
|
|
38
|
+
}
|
|
7
39
|
async function retryTransientOriginFailure(operation) {
|
|
8
40
|
try {
|
|
9
41
|
return await operation();
|
|
@@ -30,6 +62,15 @@ function scanCreationMetadata(value) {
|
|
|
30
62
|
upgradeMessage: value.upgradeMessage
|
|
31
63
|
};
|
|
32
64
|
}
|
|
65
|
+
function activeScan(value) {
|
|
66
|
+
return value.status === "queued" || value.status === "running" || value.status === "finalizing";
|
|
67
|
+
}
|
|
68
|
+
function hasPreConsentPreview(value) {
|
|
69
|
+
return Boolean(value.preConsentPreview && typeof value.preConsentPreview === "object" && !Array.isArray(value.preConsentPreview));
|
|
70
|
+
}
|
|
71
|
+
function delay(ms) {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
}
|
|
33
74
|
function toolContract(name) {
|
|
34
75
|
const contract = certScoreMcpToolContracts.find((candidate) => candidate.name === name);
|
|
35
76
|
if (!contract) {
|
|
@@ -43,15 +84,161 @@ function toolContract(name) {
|
|
|
43
84
|
annotations: contract.annotations
|
|
44
85
|
};
|
|
45
86
|
}
|
|
87
|
+
function boundedTelemetryToken(value, maxLength) {
|
|
88
|
+
return typeof value === "string" && /^[a-zA-Z0-9_.:-]+$/.test(value) && value.length <= maxLength
|
|
89
|
+
? value
|
|
90
|
+
: null;
|
|
91
|
+
}
|
|
92
|
+
function telemetryResultRecord(result) {
|
|
93
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
94
|
+
return {};
|
|
95
|
+
const toolResult = result;
|
|
96
|
+
if (toolResult.structuredContent && typeof toolResult.structuredContent === "object" && !Array.isArray(toolResult.structuredContent)) {
|
|
97
|
+
return toolResult.structuredContent;
|
|
98
|
+
}
|
|
99
|
+
const firstText = Array.isArray(toolResult.content)
|
|
100
|
+
? toolResult.content.find((item) => item && typeof item === "object" && item.type === "text")
|
|
101
|
+
: null;
|
|
102
|
+
if (!firstText || typeof firstText.text !== "string")
|
|
103
|
+
return {};
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(firstText.text);
|
|
106
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function telemetryHostname(value) {
|
|
113
|
+
if (typeof value !== "string")
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
117
|
+
return parsed.hostname.toLowerCase().replace(/\.$/, "").slice(0, 253) || null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function telemetryUrl(value) {
|
|
124
|
+
if (typeof value !== "string")
|
|
125
|
+
return null;
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
128
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
129
|
+
return null;
|
|
130
|
+
return parsed.origin.slice(0, 512);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function requestedTelemetryResource(args) {
|
|
137
|
+
const scanId = boundedTelemetryToken(args.scanId, 128);
|
|
138
|
+
if (scanId)
|
|
139
|
+
return { requestedResource: scanId, requestedResourceType: "scan_id" };
|
|
140
|
+
const jobId = boundedTelemetryToken(args.jobId, 128);
|
|
141
|
+
if (jobId)
|
|
142
|
+
return { requestedResource: jobId, requestedResourceType: "job_id" };
|
|
143
|
+
const url = telemetryUrl(args.url);
|
|
144
|
+
if (url)
|
|
145
|
+
return { requestedResource: url, requestedResourceType: "url" };
|
|
146
|
+
const domain = telemetryHostname(args.domain);
|
|
147
|
+
if (domain)
|
|
148
|
+
return { requestedResource: domain, requestedResourceType: "domain" };
|
|
149
|
+
return { requestedResource: null, requestedResourceType: null };
|
|
150
|
+
}
|
|
151
|
+
function isCertScoreCanaryUrl(value) {
|
|
152
|
+
if (typeof value !== "string")
|
|
153
|
+
return false;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
|
|
156
|
+
return parsed.pathname.startsWith("/.well-known/certscore-canary/");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function projectMcpToolInvocationObservation(input) {
|
|
163
|
+
const args = input.args && typeof input.args === "object" && !Array.isArray(input.args)
|
|
164
|
+
? input.args
|
|
165
|
+
: {};
|
|
166
|
+
const result = telemetryResultRecord(input.result);
|
|
167
|
+
const error = result.error && typeof result.error === "object" && !Array.isArray(result.error)
|
|
168
|
+
? result.error
|
|
169
|
+
: null;
|
|
170
|
+
const errorCode = boundedTelemetryToken(error?.code ?? result.errorCode, 100);
|
|
171
|
+
const rateLimited = errorCode === "rate_limited" || result.status === "rate_limited";
|
|
172
|
+
const isError = Boolean(input.result?.isError) || Boolean(error);
|
|
173
|
+
const outcome = rateLimited ? "rate_limited" : isError ? "error" : "success";
|
|
174
|
+
const resultScanId = boundedTelemetryToken(result.scanId ?? result.scan_id ?? result.jobId, 128);
|
|
175
|
+
const inputScanId = boundedTelemetryToken(args.scanId, 128);
|
|
176
|
+
const requestedResource = requestedTelemetryResource(args);
|
|
177
|
+
const targetHostname = input.toolName === "certscore_scan_site"
|
|
178
|
+
? telemetryHostname(args.url)
|
|
179
|
+
: input.toolName === "certscore_get_latest_domain_scan"
|
|
180
|
+
|| input.toolName === "certscore_get_latest_domain_pre_consent_cookies_trackers"
|
|
181
|
+
? telemetryHostname(args.domain)
|
|
182
|
+
: null;
|
|
183
|
+
const isCanary = isCertScoreCanaryUrl(args.url);
|
|
184
|
+
const executionMode = result.executionMode;
|
|
185
|
+
const scanDecision = input.toolName !== "certscore_scan_site"
|
|
186
|
+
? "not_applicable"
|
|
187
|
+
: outcome !== "success"
|
|
188
|
+
? "unavailable"
|
|
189
|
+
: result.reused === true || executionMode === "reused_scan"
|
|
190
|
+
? "reused"
|
|
191
|
+
: result.reused === false || executionMode === "new_scan" || result.quotaConsumed === true
|
|
192
|
+
? "new"
|
|
193
|
+
: "unavailable";
|
|
194
|
+
return {
|
|
195
|
+
durationMs: Math.max(0, Math.min(Math.round(input.durationMs), 3_600_000)),
|
|
196
|
+
errorCode: rateLimited ? "rate_limited" : errorCode,
|
|
197
|
+
freshness: args.freshness === "refresh" ? "refresh" : input.toolName === "certscore_scan_site" ? "latest" : null,
|
|
198
|
+
isCanary,
|
|
199
|
+
outcome,
|
|
200
|
+
quotaOutcome: rateLimited ? "rate_limited" : "allowed",
|
|
201
|
+
...requestedResource,
|
|
202
|
+
scanDecision,
|
|
203
|
+
scanFrom: args.scanFrom === "eu_de" || args.scanFrom === "eu_ie" || args.scanFrom === "california"
|
|
204
|
+
? args.scanFrom
|
|
205
|
+
: result.scanFrom === "eu_de" || result.scanFrom === "eu_ie" || result.scanFrom === "california"
|
|
206
|
+
? result.scanFrom
|
|
207
|
+
: null,
|
|
208
|
+
scanId: resultScanId ?? inputScanId,
|
|
209
|
+
scanStatus: boundedTelemetryToken(result.status, 64),
|
|
210
|
+
targetHostname,
|
|
211
|
+
toolName: input.toolName,
|
|
212
|
+
transportOutcome: isError ? "mcp_error" : "mcp_result",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function observeToolInvocation(observer, observation, requestContext) {
|
|
216
|
+
if (!observer)
|
|
217
|
+
return;
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
Promise.resolve().then(() => observer(observation, requestContext)).catch((error) => {
|
|
220
|
+
console.error("[certscore-mcp] telemetry observer failed", {
|
|
221
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
222
|
+
toolName: observation.toolName,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
46
227
|
export function createCertScoreMcpServer(options = {}) {
|
|
47
|
-
const
|
|
228
|
+
const createClient = (forwardedClientIp, anonymousRequesterSession) => new CertScoreClient({
|
|
48
229
|
apiKey: options.apiKey,
|
|
49
230
|
baseUrl: options.baseUrl,
|
|
50
231
|
clientName: "mcp",
|
|
51
|
-
forwardedClientIp
|
|
232
|
+
forwardedClientIp,
|
|
52
233
|
anonymousRequesterSecret: options.anonymousRequesterSecret,
|
|
234
|
+
anonymousSurface: options.anonymousSurface,
|
|
235
|
+
anonymousRequesterSession,
|
|
53
236
|
timeout: options.timeout
|
|
54
237
|
});
|
|
238
|
+
const client = createClient(options.forwardedClientIp, options.resolveAnonymousRequesterSession?.());
|
|
239
|
+
const clientForRequest = (extra) => options.resolveForwardedClientIp
|
|
240
|
+
? createClient(options.resolveForwardedClientIp(extra.requestInfo?.headers ?? {}), options.resolveAnonymousRequesterSession?.())
|
|
241
|
+
: client;
|
|
55
242
|
const server = new McpServer({
|
|
56
243
|
name: "certscore",
|
|
57
244
|
version: CERTSCORE_MCP_VERSION
|
|
@@ -61,60 +248,145 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
61
248
|
? toInvalidArgumentsToolError(message)
|
|
62
249
|
: sdkCreateToolError(message);
|
|
63
250
|
const lightTools = new Set(["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle"]);
|
|
251
|
+
const scanIdTools = new Set([
|
|
252
|
+
"certscore_explain_finding",
|
|
253
|
+
"certscore_export_findings",
|
|
254
|
+
"certscore_get_evidence",
|
|
255
|
+
"certscore_get_pre_consent_cookies_trackers",
|
|
256
|
+
"certscore_get_report",
|
|
257
|
+
"certscore_get_scan",
|
|
258
|
+
"certscore_get_scan_bundle",
|
|
259
|
+
"certscore_get_scan_status",
|
|
260
|
+
"certscore_list_findings",
|
|
261
|
+
]);
|
|
64
262
|
const registerMcpTool = server.registerTool.bind(server);
|
|
65
263
|
const registerTool = (name, contract, handler) => {
|
|
66
264
|
if (options.toolProfile === "light" && !lightTools.has(name)) {
|
|
67
265
|
return;
|
|
68
266
|
}
|
|
69
|
-
|
|
267
|
+
const typedHandler = handler;
|
|
268
|
+
registerMcpTool(name, contract, async (input, extra) => {
|
|
269
|
+
const startedAt = Date.now();
|
|
270
|
+
try {
|
|
271
|
+
const scanId = input && typeof input === "object" && !Array.isArray(input)
|
|
272
|
+
? input.scanId
|
|
273
|
+
: null;
|
|
274
|
+
const result = scanIdTools.has(name) && !isCanonicalScanId(scanId)
|
|
275
|
+
? toInvalidScanIdToolError()
|
|
276
|
+
: await typedHandler(input, extra);
|
|
277
|
+
observeToolInvocation(options.onToolInvocation, projectMcpToolInvocationObservation({
|
|
278
|
+
args: input,
|
|
279
|
+
durationMs: Date.now() - startedAt,
|
|
280
|
+
result,
|
|
281
|
+
toolName: name,
|
|
282
|
+
}), { headers: extra.requestInfo?.headers ?? null });
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
observeToolInvocation(options.onToolInvocation, {
|
|
287
|
+
...projectMcpToolInvocationObservation({
|
|
288
|
+
args: input,
|
|
289
|
+
durationMs: Date.now() - startedAt,
|
|
290
|
+
result: { isError: true, structuredContent: { error: { code: "handler_exception" } } },
|
|
291
|
+
toolName: name,
|
|
292
|
+
}),
|
|
293
|
+
errorCode: "handler_exception",
|
|
294
|
+
outcome: "error",
|
|
295
|
+
transportOutcome: "mcp_error",
|
|
296
|
+
}, { headers: extra.requestInfo?.headers ?? null });
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
70
300
|
};
|
|
71
|
-
registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input) => {
|
|
301
|
+
registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input, extra) => {
|
|
302
|
+
const toolStartedAtMs = Date.now();
|
|
303
|
+
const creationStartedAtMs = toolStartedAtMs;
|
|
304
|
+
const client = clientForRequest(extra);
|
|
305
|
+
const demoSubstitution = exampleDomainDemoSubstitution(input.url, options.exampleDomainDemoUrl);
|
|
306
|
+
const effectiveUrl = demoSubstitution?.effectiveUrl ?? input.url;
|
|
72
307
|
try {
|
|
73
|
-
const created = await client.scans.create(
|
|
308
|
+
const created = await client.scans.create(effectiveUrl, {
|
|
74
309
|
freshness: input.freshness ?? "latest",
|
|
75
310
|
scanFrom: input.scanFrom
|
|
76
311
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
312
|
+
console.log(JSON.stringify({
|
|
313
|
+
event: "mcp.certscore_scan_site.creation_completed",
|
|
314
|
+
durationMs: Date.now() - creationStartedAtMs,
|
|
315
|
+
executionMode: created.executionMode ?? null,
|
|
316
|
+
hasScanId: Boolean(created.scanId ?? created.scan_id),
|
|
317
|
+
reused: created.reused === true,
|
|
318
|
+
status: created.status ?? null,
|
|
319
|
+
}));
|
|
320
|
+
let initialResult = created;
|
|
321
|
+
const stableScanId = typeof created.scanId === "string" && created.scanId
|
|
322
|
+
? created.scanId
|
|
323
|
+
: typeof created.scan_id === "string" && created.scan_id
|
|
324
|
+
? created.scan_id
|
|
325
|
+
: typeof created.jobId === "string" && created.jobId
|
|
326
|
+
? created.jobId
|
|
327
|
+
: null;
|
|
328
|
+
const configuredPreviewWaitMs = options.toolProfile === "light"
|
|
329
|
+
? Math.min(Math.max(0, options.initialPreConsentPreviewWaitMs ?? 10_000), MCP_SCAN_SITE_MAX_PREVIEW_WAIT_MS)
|
|
330
|
+
: 0;
|
|
331
|
+
const totalRemainingMs = Math.max(0, toolStartedAtMs + MCP_SCAN_SITE_TARGET_RESPONSE_MS - MCP_SCAN_SITE_TARGET_RESPONSE_RESERVE_MS - Date.now());
|
|
332
|
+
const previewWaitMs = Math.min(configuredPreviewWaitMs, totalRemainingMs);
|
|
333
|
+
if (stableScanId && previewWaitMs > 0 && activeScan(initialResult)) {
|
|
334
|
+
const previewWaitStartedAtMs = Date.now();
|
|
335
|
+
const previewDeadlineMs = previewWaitStartedAtMs + previewWaitMs;
|
|
336
|
+
let internalReadCount = 0;
|
|
337
|
+
try {
|
|
338
|
+
while (Date.now() < previewDeadlineMs && activeScan(initialResult) && !hasPreConsentPreview(initialResult)) {
|
|
339
|
+
await delay(Math.min(MCP_SCAN_SITE_PREVIEW_POLL_INTERVAL_MS, previewDeadlineMs - Date.now()));
|
|
340
|
+
const requestRemainingMs = previewDeadlineMs - Date.now();
|
|
341
|
+
if (requestRemainingMs <= 0)
|
|
342
|
+
break;
|
|
343
|
+
const waitAbortController = new AbortController();
|
|
344
|
+
const waitAbortTimer = setTimeout(() => waitAbortController.abort(), requestRemainingMs);
|
|
345
|
+
try {
|
|
346
|
+
internalReadCount += 1;
|
|
347
|
+
const status = await client.scans.status(stableScanId, {
|
|
348
|
+
internalMcpOperation: { operation: "scan_site_wait", scanId: stableScanId },
|
|
349
|
+
signal: waitAbortController.signal,
|
|
350
|
+
});
|
|
351
|
+
initialResult = {
|
|
352
|
+
...status,
|
|
353
|
+
...scanCreationMetadata(created),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
clearTimeout(waitAbortTimer);
|
|
358
|
+
}
|
|
108
359
|
}
|
|
109
360
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
361
|
+
catch (error) {
|
|
362
|
+
console.warn(JSON.stringify({
|
|
363
|
+
event: "mcp.certscore_scan_site.preview_wait_deferred",
|
|
364
|
+
durationMs: Date.now() - previewWaitStartedAtMs,
|
|
365
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
366
|
+
hasPreview: hasPreConsentPreview(initialResult),
|
|
367
|
+
internalReadCount,
|
|
368
|
+
scanId: stableScanId,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
console.log(JSON.stringify({
|
|
372
|
+
event: "mcp.certscore_scan_site.preview_wait_completed",
|
|
373
|
+
durationMs: Date.now() - previewWaitStartedAtMs,
|
|
374
|
+
hasPreview: hasPreConsentPreview(initialResult),
|
|
375
|
+
internalReadCount,
|
|
376
|
+
scanId: stableScanId,
|
|
377
|
+
status: initialResult.status ?? null,
|
|
378
|
+
totalDurationMs: Date.now() - toolStartedAtMs,
|
|
379
|
+
}));
|
|
115
380
|
}
|
|
381
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(initialResult, "unknown", "scan_creation"), demoSubstitution);
|
|
382
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
116
383
|
}
|
|
117
384
|
catch (error) {
|
|
385
|
+
console.warn(JSON.stringify({
|
|
386
|
+
event: "mcp.certscore_scan_site.creation_failed",
|
|
387
|
+
durationMs: Date.now() - creationStartedAtMs,
|
|
388
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
389
|
+
}));
|
|
118
390
|
return toToolError(error);
|
|
119
391
|
}
|
|
120
392
|
});
|
|
@@ -126,41 +398,17 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
126
398
|
return toToolError(error);
|
|
127
399
|
}
|
|
128
400
|
});
|
|
129
|
-
registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }) => {
|
|
401
|
+
registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }, extra) => {
|
|
402
|
+
const client = clientForRequest(extra);
|
|
130
403
|
try {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
status: scan.status,
|
|
140
|
-
domain: scan.domain,
|
|
141
|
-
url: scan.url ?? null,
|
|
142
|
-
resultDisposition: scan.resultDisposition,
|
|
143
|
-
noGo: scan.noGo,
|
|
144
|
-
createdAt: scan.createdAt ?? null,
|
|
145
|
-
startedAt: scan.startedAt,
|
|
146
|
-
completedAt: scan.completedAt,
|
|
147
|
-
scanTimeSeconds: scan.scanTimeSeconds,
|
|
148
|
-
score: scan.score ?? null,
|
|
149
|
-
scoreStatus: scan.scoreStatus,
|
|
150
|
-
scoreVersion: scan.scoreVersion ?? null,
|
|
151
|
-
scoreUpdatedAt: scan.scoreUpdatedAt ?? null,
|
|
152
|
-
riskLevel: scan.riskLevel ?? null,
|
|
153
|
-
coverage: scan.coverage ?? null,
|
|
154
|
-
reportUrl: scan.links?.report ?? status.reportUrl ?? null,
|
|
155
|
-
links: { ...status.links, ...scan.links }
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Preserve the API status response if the terminal scan resource is
|
|
160
|
-
// briefly unavailable during eventual-consistency windows.
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return toToolResult(withMcpAgentGuidance(status));
|
|
404
|
+
const internalMcpOperation = { operation: "scan_status", scanId };
|
|
405
|
+
const status = await client.scans.status(scanId, { internalMcpOperation });
|
|
406
|
+
const guided = withMcpScanProvenanceGuidance({
|
|
407
|
+
...status,
|
|
408
|
+
jobId: undefined,
|
|
409
|
+
scanFrom: status.scanFrom ?? null
|
|
410
|
+
}, "existing_scan_retrieved");
|
|
411
|
+
return toToolResult(guided, scanStatusText(guided));
|
|
164
412
|
}
|
|
165
413
|
catch (error) {
|
|
166
414
|
return toToolError(error);
|
|
@@ -178,7 +426,16 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
178
426
|
detail: normalizeDetail(detail),
|
|
179
427
|
format: "json"
|
|
180
428
|
});
|
|
181
|
-
|
|
429
|
+
if (typeof result === "string") {
|
|
430
|
+
const guided = withMcpAgentGuidance({
|
|
431
|
+
type: "certscore_pulse_markdown",
|
|
432
|
+
scanId,
|
|
433
|
+
value: result
|
|
434
|
+
}, "existing_scan_retrieved");
|
|
435
|
+
return toToolResult(guided, markdownReportText(guided));
|
|
436
|
+
}
|
|
437
|
+
const guided = withMcpAgentGuidance(result, "existing_scan_retrieved");
|
|
438
|
+
return toToolResult(guided, pulseReportText(guided));
|
|
182
439
|
}
|
|
183
440
|
catch (error) {
|
|
184
441
|
return toToolError(error);
|
|
@@ -186,49 +443,65 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
186
443
|
});
|
|
187
444
|
registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }) => {
|
|
188
445
|
try {
|
|
189
|
-
|
|
446
|
+
const bounded = boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" }), MAX_EVIDENCE_PACKET_CHARS - 2_500);
|
|
447
|
+
const guided = withMcpAgentGuidance(bounded, "existing_scan_retrieved");
|
|
448
|
+
return toToolResult(guided, pulseReportText(guided, "CertScore evidence result"));
|
|
190
449
|
}
|
|
191
450
|
catch (error) {
|
|
192
451
|
return toToolError(error);
|
|
193
452
|
}
|
|
194
453
|
});
|
|
195
|
-
registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }) => {
|
|
454
|
+
registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }, extra) => {
|
|
455
|
+
const client = clientForRequest(extra);
|
|
196
456
|
try {
|
|
197
|
-
const
|
|
457
|
+
const responseCeilingBytes = options.toolProfile === "light"
|
|
458
|
+
? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
|
|
459
|
+
: 200_000;
|
|
460
|
+
const requestedMaxBytes = maxBytes ?? (options.toolProfile === "light"
|
|
461
|
+
? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
|
|
462
|
+
: 50_000);
|
|
463
|
+
const internalMcpOperation = { operation: "scan_bundle", scanId };
|
|
464
|
+
const scan = await retryTransientOriginFailure(() => client.scans.get(scanId, { internalMcpOperation }));
|
|
198
465
|
if (scan.status === "completed_limited" && scan.resultDisposition === "no_go") {
|
|
199
|
-
|
|
466
|
+
const bundle = buildScanBundle({
|
|
200
467
|
detail,
|
|
201
468
|
evidence: null,
|
|
202
469
|
findings: { type: "certscore_finding_list", scanId, findings: [] },
|
|
203
|
-
maxBytes,
|
|
470
|
+
maxBytes: requestedMaxBytes,
|
|
204
471
|
maxFindings,
|
|
205
472
|
maxPreConsentRows,
|
|
206
473
|
preConsentCookiesTrackers: null,
|
|
207
474
|
report: null,
|
|
475
|
+
requestedMaxBytes,
|
|
476
|
+
responseCeilingBytes,
|
|
208
477
|
scan
|
|
209
|
-
})
|
|
478
|
+
});
|
|
479
|
+
return toToolResult(bundle, scanBundleText(bundle));
|
|
210
480
|
}
|
|
211
481
|
const includeEvidence = detail === "evidence" || detail === "full";
|
|
212
482
|
const reportDetail = detail === "full" ? "full" : includeEvidence ? "evidence" : "summary";
|
|
213
483
|
const [report, findings, preConsentCookiesTrackers] = await Promise.all([
|
|
214
|
-
retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json" })),
|
|
215
|
-
retryTransientOriginFailure(() => client.findings.list(scanId)),
|
|
216
|
-
|
|
217
|
-
? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId))
|
|
484
|
+
retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json", internalMcpOperation })),
|
|
485
|
+
retryTransientOriginFailure(() => client.findings.list(scanId, { internalMcpOperation })),
|
|
486
|
+
scan.status === "completed"
|
|
487
|
+
? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId, { internalMcpOperation }))
|
|
218
488
|
: Promise.resolve(null)
|
|
219
489
|
]);
|
|
220
490
|
const evidence = includeEvidence ? report : null;
|
|
221
|
-
|
|
491
|
+
const bundle = buildScanBundle({
|
|
222
492
|
detail,
|
|
223
493
|
evidence,
|
|
224
494
|
findings,
|
|
225
|
-
maxBytes,
|
|
495
|
+
maxBytes: requestedMaxBytes,
|
|
226
496
|
maxFindings,
|
|
227
497
|
maxPreConsentRows,
|
|
228
498
|
preConsentCookiesTrackers,
|
|
229
499
|
report,
|
|
500
|
+
requestedMaxBytes,
|
|
501
|
+
responseCeilingBytes,
|
|
230
502
|
scan
|
|
231
|
-
})
|
|
503
|
+
});
|
|
504
|
+
return toToolResult(bundle, scanBundleText(bundle));
|
|
232
505
|
}
|
|
233
506
|
catch (error) {
|
|
234
507
|
return toToolError(error);
|
|
@@ -237,7 +510,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
237
510
|
registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }) => {
|
|
238
511
|
try {
|
|
239
512
|
const report = await client.getScan(scanId, { detail: "full", format: "json" });
|
|
240
|
-
|
|
513
|
+
const guided = withMcpAgentGuidance(exportFindings(report), "existing_scan_retrieved");
|
|
514
|
+
return toToolResult(guided, findingListText(guided, "Exported canonical projected findings"));
|
|
241
515
|
}
|
|
242
516
|
catch (error) {
|
|
243
517
|
return toToolError(error);
|
|
@@ -245,7 +519,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
245
519
|
});
|
|
246
520
|
registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }) => {
|
|
247
521
|
try {
|
|
248
|
-
|
|
522
|
+
const guided = withMcpAgentGuidance(paginateFindingList(await client.findings.list(scanId), { limit, offset }), "existing_scan_retrieved");
|
|
523
|
+
return toToolResult(guided, findingListText(guided));
|
|
249
524
|
}
|
|
250
525
|
catch (error) {
|
|
251
526
|
return toToolError(error);
|
|
@@ -253,7 +528,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
253
528
|
});
|
|
254
529
|
registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
|
|
255
530
|
try {
|
|
256
|
-
|
|
531
|
+
const guided = withMcpAgentGuidance(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }), "existing_scan_retrieved");
|
|
532
|
+
return toToolResult(guided, preConsentInventoryText(guided));
|
|
257
533
|
}
|
|
258
534
|
catch (error) {
|
|
259
535
|
return toToolError(error);
|
|
@@ -277,7 +553,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
277
553
|
});
|
|
278
554
|
registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
|
|
279
555
|
try {
|
|
280
|
-
|
|
556
|
+
const guided = withMcpAgentGuidance(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }), "existing_scan_retrieved");
|
|
557
|
+
return toToolResult(guided, preConsentInventoryText(guided));
|
|
281
558
|
}
|
|
282
559
|
catch (error) {
|
|
283
560
|
return toToolError(error);
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { type FindingList, type JobStatus, type PreConsentCookiesTrackers, type PulseDetail, type PulseFormat, type PulseResult, type ScanResource, type TopFinding } from "@certscore/sdk";
|
|
3
3
|
export declare const MAX_EVIDENCE_PACKET_CHARS = 250000;
|
|
4
|
+
type McpPollGuidanceContext = "scan_creation" | "scan_status" | "upstream";
|
|
5
|
+
type ScanProvenanceMode = "new_scan_started" | "existing_completed_scan_reused" | "existing_scan_retrieved" | "unknown";
|
|
4
6
|
type ActionableError = {
|
|
5
7
|
code: string;
|
|
6
8
|
message: string;
|
|
@@ -10,10 +12,22 @@ type ActionableError = {
|
|
|
10
12
|
field?: string;
|
|
11
13
|
mcpCode?: number;
|
|
12
14
|
};
|
|
13
|
-
export declare function toToolResult(payload: unknown): CallToolResult;
|
|
15
|
+
export declare function toToolResult(payload: unknown, text?: string): CallToolResult;
|
|
14
16
|
export declare function toToolError(error: unknown): CallToolResult;
|
|
15
17
|
export declare function toInvalidArgumentsToolError(errorMessage: string): CallToolResult;
|
|
16
|
-
export declare function
|
|
18
|
+
export declare function toInvalidScanIdToolError(): CallToolResult;
|
|
19
|
+
export declare function withMcpAgentGuidance<T extends Record<string, any>>(value: T, fallbackProvenanceMode?: ScanProvenanceMode, pollGuidanceContext?: McpPollGuidanceContext): T & {
|
|
20
|
+
error: ActionableError | null;
|
|
21
|
+
observationOnlyDisclaimer: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function withMcpScanProvenanceGuidance(value: Record<string, any>, fallbackProvenanceMode: ScanProvenanceMode): {
|
|
24
|
+
interpretationGuidance: {
|
|
25
|
+
scoreLabel: "CertScore score";
|
|
26
|
+
observableSignalsOnly: true;
|
|
27
|
+
doNotInferUnobservedTechnologies: true;
|
|
28
|
+
doNotInferLegalComplianceStatus: true;
|
|
29
|
+
statement: string;
|
|
30
|
+
};
|
|
17
31
|
error: ActionableError | null;
|
|
18
32
|
observationOnlyDisclaimer: string;
|
|
19
33
|
};
|
|
@@ -51,6 +65,13 @@ export declare function paginateFindingList<T extends Record<string, unknown>>(p
|
|
|
51
65
|
export declare function limitPreConsentRows<T extends Record<string, unknown>>(payload: T, options?: {
|
|
52
66
|
maxRows?: number;
|
|
53
67
|
}): T;
|
|
68
|
+
export declare function scanStatusText(value: Record<string, any>): string;
|
|
69
|
+
export declare function scanSiteText(value: Record<string, any>, leadingLines?: string[]): string;
|
|
70
|
+
export declare function findingListText(value: Record<string, any>, label?: string): string;
|
|
71
|
+
export declare function preConsentInventoryText(value: Record<string, any>): string;
|
|
72
|
+
export declare function pulseReportText(value: Record<string, any>, label?: string): string;
|
|
73
|
+
export declare function markdownReportText(value: Record<string, any>): string;
|
|
74
|
+
export declare function scanBundleText(bundle: Record<string, any>): string;
|
|
54
75
|
export declare function buildScanBundle(input: {
|
|
55
76
|
detail?: "summary" | "findings" | "evidence" | "full";
|
|
56
77
|
evidence?: PulseResult | null;
|
|
@@ -60,6 +81,8 @@ export declare function buildScanBundle(input: {
|
|
|
60
81
|
maxPreConsentRows?: number;
|
|
61
82
|
preConsentCookiesTrackers?: PreConsentCookiesTrackers | null;
|
|
62
83
|
report: PulseResult | null;
|
|
84
|
+
requestedMaxBytes?: number;
|
|
85
|
+
responseCeilingBytes?: number;
|
|
63
86
|
scan: ScanResource;
|
|
64
87
|
}): Record<string, any>;
|
|
65
88
|
export declare function explainFinding(report: PulseResult, findingId: string): {
|