@certscore/mcp 0.2.12 → 0.2.16
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 +151 -45
- package/dist/certscore-mcp.mjs +4377 -3997
- package/dist/index.js +1 -1
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +388 -124
- package/dist/tools.d.ts +43 -59
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +1303 -65
- package/package.json +13 -12
- package/server-light.json +21 -6
- package/server.json +2 -2
package/dist/server.js
CHANGED
|
@@ -1,10 +1,64 @@
|
|
|
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,
|
|
6
|
-
|
|
7
|
-
const
|
|
5
|
+
import { boundEvidencePacket, buildScanBundle, exportFindings, findingListText, limitPreConsentRows, markdownReportText, MAX_EVIDENCE_PACKET_CHARS, normalizeDetail, normalizeFormat, paginateFindingList, preConsentInventoryText, pulseReportText, scanBundleText, scanStatusText, toInvalidArgumentsToolError, toInvalidScanIdToolError, toToolError, toToolResult, withMcpAgentGuidance, withMcpScanProvenanceGuidance } from "./tools.js";
|
|
6
|
+
const DEFAULT_MCP_SCAN_TOOL_BUDGET_MS = 25_000;
|
|
7
|
+
const MAX_MCP_SCAN_TOOL_BUDGET_MS = 45_000;
|
|
8
|
+
const MCP_SCAN_TOOL_RESPONSE_RESERVE_MS = 1_000;
|
|
9
|
+
const LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES = 25_000;
|
|
10
|
+
export function resolveMcpScanSiteWaitBudget(input) {
|
|
11
|
+
const totalBudgetMs = Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_TOOL_BUDGET_MS, MAX_MCP_SCAN_TOOL_BUDGET_MS);
|
|
12
|
+
const elapsedMs = Math.max(0, input.nowMs - input.startedAtMs);
|
|
13
|
+
return {
|
|
14
|
+
elapsedMs,
|
|
15
|
+
remainingWaitMs: Math.max(0, totalBudgetMs - elapsedMs - MCP_SCAN_TOOL_RESPONSE_RESERVE_MS),
|
|
16
|
+
responseReserveMs: MCP_SCAN_TOOL_RESPONSE_RESERVE_MS,
|
|
17
|
+
totalBudgetMs,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function exampleDomainDemoSubstitution(requestedUrl, demoUrl) {
|
|
21
|
+
if (!demoUrl)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(requestedUrl.includes("://") ? requestedUrl : `https://${requestedUrl}`);
|
|
25
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
26
|
+
const reserved = ["example.com", "example.net", "example.org"].some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
27
|
+
if (!reserved)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
requestedUrl,
|
|
31
|
+
effectiveUrl: demoUrl,
|
|
32
|
+
reason: "iana_example_domain",
|
|
33
|
+
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."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function withExampleDomainDemo(value, substitution) {
|
|
41
|
+
return substitution ? { ...value, demoSubstitution: substitution } : value;
|
|
42
|
+
}
|
|
43
|
+
function exampleDomainDemoText(value, substitution) {
|
|
44
|
+
if (!substitution)
|
|
45
|
+
return undefined;
|
|
46
|
+
const status = typeof value.status === "string" ? ` Status=${value.status}.` : "";
|
|
47
|
+
const scanId = typeof value.scanId === "string" ? ` ScanId=${value.scanId}.` : "";
|
|
48
|
+
return `${substitution.message}${status}${scanId} Full result and substitution provenance are in structuredContent.`;
|
|
49
|
+
}
|
|
50
|
+
async function retryTransientOriginFailure(operation) {
|
|
51
|
+
try {
|
|
52
|
+
return await operation();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const retryable = error instanceof Error && "status" in error && [502, 503, 504].includes(Number(error.status));
|
|
56
|
+
if (!retryable) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return operation();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
8
62
|
function scanCreationMetadata(value) {
|
|
9
63
|
return {
|
|
10
64
|
executionMode: value.executionMode,
|
|
@@ -32,99 +86,291 @@ function toolContract(name) {
|
|
|
32
86
|
annotations: contract.annotations
|
|
33
87
|
};
|
|
34
88
|
}
|
|
89
|
+
function boundedTelemetryToken(value, maxLength) {
|
|
90
|
+
return typeof value === "string" && /^[a-zA-Z0-9_.:-]+$/.test(value) && value.length <= maxLength
|
|
91
|
+
? value
|
|
92
|
+
: null;
|
|
93
|
+
}
|
|
94
|
+
function telemetryResultRecord(result) {
|
|
95
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
96
|
+
return {};
|
|
97
|
+
const toolResult = result;
|
|
98
|
+
if (toolResult.structuredContent && typeof toolResult.structuredContent === "object" && !Array.isArray(toolResult.structuredContent)) {
|
|
99
|
+
return toolResult.structuredContent;
|
|
100
|
+
}
|
|
101
|
+
const firstText = Array.isArray(toolResult.content)
|
|
102
|
+
? toolResult.content.find((item) => item && typeof item === "object" && item.type === "text")
|
|
103
|
+
: null;
|
|
104
|
+
if (!firstText || typeof firstText.text !== "string")
|
|
105
|
+
return {};
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(firstText.text);
|
|
108
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function telemetryHostname(value) {
|
|
115
|
+
if (typeof value !== "string")
|
|
116
|
+
return null;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
119
|
+
return parsed.hostname.toLowerCase().replace(/\.$/, "").slice(0, 253) || null;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function telemetryUrl(value) {
|
|
126
|
+
if (typeof value !== "string")
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
130
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
131
|
+
return null;
|
|
132
|
+
return parsed.origin.slice(0, 512);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function requestedTelemetryResource(args) {
|
|
139
|
+
const scanId = boundedTelemetryToken(args.scanId, 128);
|
|
140
|
+
if (scanId)
|
|
141
|
+
return { requestedResource: scanId, requestedResourceType: "scan_id" };
|
|
142
|
+
const jobId = boundedTelemetryToken(args.jobId, 128);
|
|
143
|
+
if (jobId)
|
|
144
|
+
return { requestedResource: jobId, requestedResourceType: "job_id" };
|
|
145
|
+
const url = telemetryUrl(args.url);
|
|
146
|
+
if (url)
|
|
147
|
+
return { requestedResource: url, requestedResourceType: "url" };
|
|
148
|
+
const domain = telemetryHostname(args.domain);
|
|
149
|
+
if (domain)
|
|
150
|
+
return { requestedResource: domain, requestedResourceType: "domain" };
|
|
151
|
+
return { requestedResource: null, requestedResourceType: null };
|
|
152
|
+
}
|
|
153
|
+
function isCertScoreCanaryUrl(value) {
|
|
154
|
+
if (typeof value !== "string")
|
|
155
|
+
return false;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
|
|
158
|
+
return parsed.pathname.startsWith("/.well-known/certscore-canary/");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function projectMcpToolInvocationObservation(input) {
|
|
165
|
+
const args = input.args && typeof input.args === "object" && !Array.isArray(input.args)
|
|
166
|
+
? input.args
|
|
167
|
+
: {};
|
|
168
|
+
const result = telemetryResultRecord(input.result);
|
|
169
|
+
const error = result.error && typeof result.error === "object" && !Array.isArray(result.error)
|
|
170
|
+
? result.error
|
|
171
|
+
: null;
|
|
172
|
+
const errorCode = boundedTelemetryToken(error?.code ?? result.errorCode, 100);
|
|
173
|
+
const rateLimited = errorCode === "rate_limited" || result.status === "rate_limited";
|
|
174
|
+
const isError = Boolean(input.result?.isError) || Boolean(error);
|
|
175
|
+
const outcome = rateLimited ? "rate_limited" : isError ? "error" : "success";
|
|
176
|
+
const resultScanId = boundedTelemetryToken(result.scanId ?? result.scan_id ?? result.jobId, 128);
|
|
177
|
+
const inputScanId = boundedTelemetryToken(args.scanId, 128);
|
|
178
|
+
const requestedResource = requestedTelemetryResource(args);
|
|
179
|
+
const targetHostname = input.toolName === "certscore_scan_site"
|
|
180
|
+
? telemetryHostname(args.url)
|
|
181
|
+
: input.toolName === "certscore_get_latest_domain_scan"
|
|
182
|
+
|| input.toolName === "certscore_get_latest_domain_pre_consent_cookies_trackers"
|
|
183
|
+
? telemetryHostname(args.domain)
|
|
184
|
+
: null;
|
|
185
|
+
const isCanary = isCertScoreCanaryUrl(args.url);
|
|
186
|
+
const executionMode = result.executionMode;
|
|
187
|
+
const scanDecision = input.toolName !== "certscore_scan_site"
|
|
188
|
+
? "not_applicable"
|
|
189
|
+
: outcome !== "success"
|
|
190
|
+
? "unavailable"
|
|
191
|
+
: result.reused === true || executionMode === "reused_scan"
|
|
192
|
+
? "reused"
|
|
193
|
+
: result.reused === false || executionMode === "new_scan" || result.quotaConsumed === true
|
|
194
|
+
? "new"
|
|
195
|
+
: "unavailable";
|
|
196
|
+
return {
|
|
197
|
+
durationMs: Math.max(0, Math.min(Math.round(input.durationMs), 3_600_000)),
|
|
198
|
+
errorCode: rateLimited ? "rate_limited" : errorCode,
|
|
199
|
+
freshness: args.freshness === "refresh" ? "refresh" : input.toolName === "certscore_scan_site" ? "latest" : null,
|
|
200
|
+
isCanary,
|
|
201
|
+
outcome,
|
|
202
|
+
quotaOutcome: rateLimited ? "rate_limited" : "allowed",
|
|
203
|
+
...requestedResource,
|
|
204
|
+
scanDecision,
|
|
205
|
+
scanFrom: args.scanFrom === "eu_de" || args.scanFrom === "eu_ie" || args.scanFrom === "california"
|
|
206
|
+
? args.scanFrom
|
|
207
|
+
: result.scanFrom === "eu_de" || result.scanFrom === "eu_ie" || result.scanFrom === "california"
|
|
208
|
+
? result.scanFrom
|
|
209
|
+
: null,
|
|
210
|
+
scanId: resultScanId ?? inputScanId,
|
|
211
|
+
scanStatus: boundedTelemetryToken(result.status, 64),
|
|
212
|
+
targetHostname,
|
|
213
|
+
toolName: input.toolName,
|
|
214
|
+
transportOutcome: isError ? "mcp_error" : "mcp_result",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function observeToolInvocation(observer, observation, requestContext) {
|
|
218
|
+
if (!observer)
|
|
219
|
+
return;
|
|
220
|
+
queueMicrotask(() => {
|
|
221
|
+
Promise.resolve().then(() => observer(observation, requestContext)).catch((error) => {
|
|
222
|
+
console.error("[certscore-mcp] telemetry observer failed", {
|
|
223
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
224
|
+
toolName: observation.toolName,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
35
229
|
export function createCertScoreMcpServer(options = {}) {
|
|
36
|
-
const
|
|
230
|
+
const createClient = (forwardedClientIp, anonymousRequesterSession) => new CertScoreClient({
|
|
37
231
|
apiKey: options.apiKey,
|
|
38
232
|
baseUrl: options.baseUrl,
|
|
39
233
|
clientName: "mcp",
|
|
40
|
-
forwardedClientIp
|
|
234
|
+
forwardedClientIp,
|
|
41
235
|
anonymousRequesterSecret: options.anonymousRequesterSecret,
|
|
236
|
+
anonymousSurface: options.anonymousSurface,
|
|
237
|
+
anonymousRequesterSession,
|
|
42
238
|
timeout: options.timeout
|
|
43
239
|
});
|
|
240
|
+
const client = createClient(options.forwardedClientIp, options.resolveAnonymousRequesterSession?.());
|
|
241
|
+
const clientForRequest = (extra) => options.resolveForwardedClientIp
|
|
242
|
+
? createClient(options.resolveForwardedClientIp(extra.requestInfo?.headers ?? {}), options.resolveAnonymousRequesterSession?.())
|
|
243
|
+
: client;
|
|
44
244
|
const server = new McpServer({
|
|
45
245
|
name: "certscore",
|
|
46
246
|
version: CERTSCORE_MCP_VERSION
|
|
47
247
|
});
|
|
48
|
-
const
|
|
248
|
+
const sdkCreateToolError = server.createToolError.bind(server);
|
|
249
|
+
server.createToolError = (message) => message.includes("Input validation error:")
|
|
250
|
+
? toInvalidArgumentsToolError(message)
|
|
251
|
+
: sdkCreateToolError(message);
|
|
252
|
+
const lightTools = new Set(["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle"]);
|
|
253
|
+
const scanIdTools = new Set([
|
|
254
|
+
"certscore_explain_finding",
|
|
255
|
+
"certscore_export_findings",
|
|
256
|
+
"certscore_get_evidence",
|
|
257
|
+
"certscore_get_pre_consent_cookies_trackers",
|
|
258
|
+
"certscore_get_report",
|
|
259
|
+
"certscore_get_scan",
|
|
260
|
+
"certscore_get_scan_bundle",
|
|
261
|
+
"certscore_get_scan_status",
|
|
262
|
+
"certscore_list_findings",
|
|
263
|
+
]);
|
|
49
264
|
const registerMcpTool = server.registerTool.bind(server);
|
|
50
265
|
const registerTool = (name, contract, handler) => {
|
|
51
266
|
if (options.toolProfile === "light" && !lightTools.has(name)) {
|
|
52
267
|
return;
|
|
53
268
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
reportUrl: result.reportUrl ?? null,
|
|
72
|
-
pulse: result.pulse ?? null,
|
|
73
|
-
resultDisposition: result.resultDisposition ?? result.pulse?.resultDisposition ?? null,
|
|
74
|
-
noGo: result.noGo ?? result.pulse?.noGo ?? null
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
registerTool("create_scan", toolContract("create_scan"), async (input) => {
|
|
78
|
-
try {
|
|
79
|
-
if (!createScanDeprecationWarningPrinted) {
|
|
80
|
-
createScanDeprecationWarningPrinted = true;
|
|
81
|
-
console.error("[certscore-mcp] create_scan is deprecated in the 0.2.x line. Use scan_site for new integrations.");
|
|
269
|
+
const typedHandler = handler;
|
|
270
|
+
registerMcpTool(name, contract, async (input, extra) => {
|
|
271
|
+
const startedAt = Date.now();
|
|
272
|
+
try {
|
|
273
|
+
const scanId = input && typeof input === "object" && !Array.isArray(input)
|
|
274
|
+
? input.scanId
|
|
275
|
+
: null;
|
|
276
|
+
const result = scanIdTools.has(name) && !isCanonicalScanId(scanId)
|
|
277
|
+
? toInvalidScanIdToolError()
|
|
278
|
+
: await typedHandler(input, extra);
|
|
279
|
+
observeToolInvocation(options.onToolInvocation, projectMcpToolInvocationObservation({
|
|
280
|
+
args: input,
|
|
281
|
+
durationMs: Date.now() - startedAt,
|
|
282
|
+
result,
|
|
283
|
+
toolName: name,
|
|
284
|
+
}), { headers: extra.requestInfo?.headers ?? null });
|
|
285
|
+
return result;
|
|
82
286
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
287
|
+
catch (error) {
|
|
288
|
+
observeToolInvocation(options.onToolInvocation, {
|
|
289
|
+
...projectMcpToolInvocationObservation({
|
|
290
|
+
args: input,
|
|
291
|
+
durationMs: Date.now() - startedAt,
|
|
292
|
+
result: { isError: true, structuredContent: { error: { code: "handler_exception" } } },
|
|
293
|
+
toolName: name,
|
|
294
|
+
}),
|
|
295
|
+
errorCode: "handler_exception",
|
|
296
|
+
outcome: "error",
|
|
297
|
+
transportOutcome: "mcp_error",
|
|
298
|
+
}, { headers: extra.requestInfo?.headers ?? null });
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input, extra) => {
|
|
304
|
+
const toolStartedAtMs = Date.now();
|
|
305
|
+
const client = clientForRequest(extra);
|
|
306
|
+
const demoSubstitution = exampleDomainDemoSubstitution(input.url, options.exampleDomainDemoUrl);
|
|
307
|
+
const effectiveUrl = demoSubstitution?.effectiveUrl ?? input.url;
|
|
90
308
|
try {
|
|
91
|
-
const created = await client.scans.create(
|
|
309
|
+
const created = await client.scans.create(effectiveUrl, {
|
|
92
310
|
freshness: input.freshness ?? "latest",
|
|
93
311
|
scanFrom: input.scanFrom
|
|
94
312
|
});
|
|
95
313
|
if (input.waitForCompletion === false || created.type === "certscore_scan") {
|
|
96
|
-
|
|
314
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
|
|
315
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
97
316
|
}
|
|
317
|
+
const waitBudget = resolveMcpScanSiteWaitBudget({
|
|
318
|
+
maxWaitSeconds: input.maxWaitSeconds,
|
|
319
|
+
nowMs: Date.now(),
|
|
320
|
+
startedAtMs: toolStartedAtMs,
|
|
321
|
+
});
|
|
322
|
+
if (waitBudget.remainingWaitMs === 0) {
|
|
323
|
+
console.warn(JSON.stringify({
|
|
324
|
+
event: "mcp.certscore_scan_site.wait_budget_consumed",
|
|
325
|
+
jobId: created.jobId ?? null,
|
|
326
|
+
scanId: created.scanId ?? created.scan_id ?? null,
|
|
327
|
+
status: created.status ?? null,
|
|
328
|
+
...waitBudget,
|
|
329
|
+
}));
|
|
330
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
|
|
331
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
332
|
+
}
|
|
333
|
+
const waitAbortController = new AbortController();
|
|
334
|
+
const waitAbortTimer = setTimeout(() => waitAbortController.abort(), waitBudget.remainingWaitMs);
|
|
98
335
|
try {
|
|
336
|
+
const internalMcpOperation = { operation: "scan_site_wait", scanId: created.scanId ?? created.scan_id ?? created.jobId };
|
|
99
337
|
const completed = await client.scans.wait(created, {
|
|
100
|
-
maxWaitMs:
|
|
338
|
+
maxWaitMs: waitBudget.remainingWaitMs,
|
|
339
|
+
internalMcpOperation,
|
|
340
|
+
signal: waitAbortController.signal,
|
|
101
341
|
});
|
|
102
|
-
|
|
342
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance({
|
|
103
343
|
...completed,
|
|
104
|
-
...scanCreationMetadata(created)
|
|
105
|
-
|
|
106
|
-
|
|
344
|
+
...scanCreationMetadata(created)
|
|
345
|
+
}), demoSubstitution);
|
|
346
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
107
347
|
}
|
|
108
348
|
catch (error) {
|
|
109
|
-
if (!(error instanceof CertScoreTimeoutError)) {
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
349
|
const scanId = created.scanId ?? created.scan_id;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
350
|
+
console.warn(JSON.stringify({
|
|
351
|
+
event: "mcp.certscore_scan_site.wait_deferred",
|
|
352
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
353
|
+
jobId: created.jobId ?? null,
|
|
354
|
+
scanId: scanId ?? null,
|
|
355
|
+
status: created.status ?? null,
|
|
356
|
+
...waitBudget,
|
|
357
|
+
}));
|
|
358
|
+
// Waiting is a convenience layered on top of scan creation. Once the
|
|
359
|
+
// API has accepted a scan, never turn a transient polling or hydration
|
|
360
|
+
// failure into an identity-less tool error that encourages callers to
|
|
361
|
+
// submit a second, non-idempotent certscore_scan_site request.
|
|
362
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
|
|
363
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
clearTimeout(waitAbortTimer);
|
|
121
367
|
}
|
|
122
368
|
}
|
|
123
369
|
catch (error) {
|
|
124
370
|
return toToolError(error);
|
|
125
371
|
}
|
|
126
372
|
});
|
|
127
|
-
registerTool("
|
|
373
|
+
registerTool("certscore_get_scan", toolContract("certscore_get_scan"), async ({ scanId }) => {
|
|
128
374
|
try {
|
|
129
375
|
return toToolResult(await client.scans.get(scanId));
|
|
130
376
|
}
|
|
@@ -132,53 +378,23 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
132
378
|
return toToolError(error);
|
|
133
379
|
}
|
|
134
380
|
});
|
|
135
|
-
registerTool("
|
|
381
|
+
registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }, extra) => {
|
|
382
|
+
const client = clientForRequest(extra);
|
|
136
383
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (needsTerminalHydration) {
|
|
141
|
-
try {
|
|
142
|
-
const scan = await client.scans.get(scanId);
|
|
143
|
-
return toToolResult({
|
|
144
|
-
...status,
|
|
145
|
-
type: "certscore_scan_job",
|
|
146
|
-
status: scan.status,
|
|
147
|
-
domain: scan.domain,
|
|
148
|
-
resultDisposition: scan.resultDisposition,
|
|
149
|
-
noGo: scan.noGo,
|
|
150
|
-
startedAt: scan.startedAt,
|
|
151
|
-
completedAt: scan.completedAt,
|
|
152
|
-
scanTimeSeconds: scan.scanTimeSeconds,
|
|
153
|
-
recommendedNextTool: "get_scan_bundle"
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// Preserve the API status response if the terminal scan resource is
|
|
158
|
-
// briefly unavailable during eventual-consistency windows.
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return toToolResult(status);
|
|
162
|
-
}
|
|
163
|
-
if (!jobId) {
|
|
164
|
-
return toToolResult({
|
|
165
|
-
error: {
|
|
166
|
-
name: "InvalidToolInput",
|
|
167
|
-
message: "Provide either scanId for API v2 scan status or jobId for Pulse job status."
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
const status = await client.getJobStatus(jobId);
|
|
172
|
-
return toToolResult({
|
|
384
|
+
const internalMcpOperation = { operation: "scan_status", scanId };
|
|
385
|
+
const status = await client.scans.status(scanId, { internalMcpOperation });
|
|
386
|
+
const guided = withMcpScanProvenanceGuidance({
|
|
173
387
|
...status,
|
|
174
|
-
|
|
175
|
-
|
|
388
|
+
jobId: undefined,
|
|
389
|
+
scanFrom: status.scanFrom ?? null
|
|
390
|
+
}, "existing_scan_retrieved");
|
|
391
|
+
return toToolResult(guided, scanStatusText(guided));
|
|
176
392
|
}
|
|
177
393
|
catch (error) {
|
|
178
394
|
return toToolError(error);
|
|
179
395
|
}
|
|
180
396
|
});
|
|
181
|
-
registerTool("
|
|
397
|
+
registerTool("certscore_get_report", toolContract("certscore_get_report"), async ({ scanId, detail, format }) => {
|
|
182
398
|
try {
|
|
183
399
|
const normalizedFormat = normalizeFormat(format);
|
|
184
400
|
const result = normalizedFormat === "markdown"
|
|
@@ -190,69 +406,116 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
190
406
|
detail: normalizeDetail(detail),
|
|
191
407
|
format: "json"
|
|
192
408
|
});
|
|
193
|
-
|
|
409
|
+
if (typeof result === "string") {
|
|
410
|
+
const guided = withMcpAgentGuidance({
|
|
411
|
+
type: "certscore_pulse_markdown",
|
|
412
|
+
scanId,
|
|
413
|
+
value: result
|
|
414
|
+
}, "existing_scan_retrieved");
|
|
415
|
+
return toToolResult(guided, markdownReportText(guided));
|
|
416
|
+
}
|
|
417
|
+
const guided = withMcpAgentGuidance(result, "existing_scan_retrieved");
|
|
418
|
+
return toToolResult(guided, pulseReportText(guided));
|
|
194
419
|
}
|
|
195
420
|
catch (error) {
|
|
196
421
|
return toToolError(error);
|
|
197
422
|
}
|
|
198
423
|
});
|
|
199
|
-
registerTool("
|
|
424
|
+
registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }) => {
|
|
200
425
|
try {
|
|
201
|
-
|
|
426
|
+
const bounded = boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" }), MAX_EVIDENCE_PACKET_CHARS - 2_500);
|
|
427
|
+
const guided = withMcpAgentGuidance(bounded, "existing_scan_retrieved");
|
|
428
|
+
return toToolResult(guided, pulseReportText(guided, "CertScore evidence result"));
|
|
202
429
|
}
|
|
203
430
|
catch (error) {
|
|
204
431
|
return toToolError(error);
|
|
205
432
|
}
|
|
206
433
|
});
|
|
207
|
-
registerTool("
|
|
434
|
+
registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }, extra) => {
|
|
435
|
+
const client = clientForRequest(extra);
|
|
208
436
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
437
|
+
const responseCeilingBytes = options.toolProfile === "light"
|
|
438
|
+
? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
|
|
439
|
+
: 200_000;
|
|
440
|
+
const requestedMaxBytes = maxBytes ?? (options.toolProfile === "light"
|
|
441
|
+
? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
|
|
442
|
+
: 50_000);
|
|
443
|
+
const internalMcpOperation = { operation: "scan_bundle", scanId };
|
|
444
|
+
const scan = await retryTransientOriginFailure(() => client.scans.get(scanId, { internalMcpOperation }));
|
|
445
|
+
if (scan.status === "completed_limited" && scan.resultDisposition === "no_go") {
|
|
446
|
+
const bundle = buildScanBundle({
|
|
447
|
+
detail,
|
|
448
|
+
evidence: null,
|
|
449
|
+
findings: { type: "certscore_finding_list", scanId, findings: [] },
|
|
450
|
+
maxBytes: requestedMaxBytes,
|
|
451
|
+
maxFindings,
|
|
452
|
+
maxPreConsentRows,
|
|
453
|
+
preConsentCookiesTrackers: null,
|
|
454
|
+
report: null,
|
|
455
|
+
requestedMaxBytes,
|
|
456
|
+
responseCeilingBytes,
|
|
457
|
+
scan
|
|
458
|
+
});
|
|
459
|
+
return toToolResult(bundle, scanBundleText(bundle));
|
|
460
|
+
}
|
|
461
|
+
const includeEvidence = detail === "evidence" || detail === "full";
|
|
462
|
+
const reportDetail = detail === "full" ? "full" : includeEvidence ? "evidence" : "summary";
|
|
463
|
+
const [report, findings, preConsentCookiesTrackers] = await Promise.all([
|
|
464
|
+
retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json", internalMcpOperation })),
|
|
465
|
+
retryTransientOriginFailure(() => client.findings.list(scanId, { internalMcpOperation })),
|
|
466
|
+
scan.status === "completed"
|
|
467
|
+
? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId, { internalMcpOperation }))
|
|
468
|
+
: Promise.resolve(null)
|
|
215
469
|
]);
|
|
216
|
-
|
|
470
|
+
const evidence = includeEvidence ? report : null;
|
|
471
|
+
const bundle = buildScanBundle({
|
|
472
|
+
detail,
|
|
217
473
|
evidence,
|
|
218
474
|
findings,
|
|
475
|
+
maxBytes: requestedMaxBytes,
|
|
219
476
|
maxFindings,
|
|
220
477
|
maxPreConsentRows,
|
|
221
478
|
preConsentCookiesTrackers,
|
|
222
479
|
report,
|
|
480
|
+
requestedMaxBytes,
|
|
481
|
+
responseCeilingBytes,
|
|
223
482
|
scan
|
|
224
|
-
})
|
|
483
|
+
});
|
|
484
|
+
return toToolResult(bundle, scanBundleText(bundle));
|
|
225
485
|
}
|
|
226
486
|
catch (error) {
|
|
227
487
|
return toToolError(error);
|
|
228
488
|
}
|
|
229
489
|
});
|
|
230
|
-
registerTool("
|
|
490
|
+
registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }) => {
|
|
231
491
|
try {
|
|
232
492
|
const report = await client.getScan(scanId, { detail: "full", format: "json" });
|
|
233
|
-
|
|
493
|
+
const guided = withMcpAgentGuidance(exportFindings(report), "existing_scan_retrieved");
|
|
494
|
+
return toToolResult(guided, findingListText(guided, "Exported canonical projected findings"));
|
|
234
495
|
}
|
|
235
496
|
catch (error) {
|
|
236
497
|
return toToolError(error);
|
|
237
498
|
}
|
|
238
499
|
});
|
|
239
|
-
registerTool("
|
|
500
|
+
registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }) => {
|
|
240
501
|
try {
|
|
241
|
-
|
|
502
|
+
const guided = withMcpAgentGuidance(paginateFindingList(await client.findings.list(scanId), { limit, offset }), "existing_scan_retrieved");
|
|
503
|
+
return toToolResult(guided, findingListText(guided));
|
|
242
504
|
}
|
|
243
505
|
catch (error) {
|
|
244
506
|
return toToolError(error);
|
|
245
507
|
}
|
|
246
508
|
});
|
|
247
|
-
registerTool("
|
|
509
|
+
registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
|
|
248
510
|
try {
|
|
249
|
-
|
|
511
|
+
const guided = withMcpAgentGuidance(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }), "existing_scan_retrieved");
|
|
512
|
+
return toToolResult(guided, preConsentInventoryText(guided));
|
|
250
513
|
}
|
|
251
514
|
catch (error) {
|
|
252
515
|
return toToolError(error);
|
|
253
516
|
}
|
|
254
517
|
});
|
|
255
|
-
registerTool("
|
|
518
|
+
registerTool("certscore_explain_finding", toolContract("certscore_explain_finding"), async ({ scanId, findingId }) => {
|
|
256
519
|
try {
|
|
257
520
|
return toToolResult(await client.findings.explain(scanId, findingId));
|
|
258
521
|
}
|
|
@@ -260,7 +523,7 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
260
523
|
return toToolError(error);
|
|
261
524
|
}
|
|
262
525
|
});
|
|
263
|
-
registerTool("
|
|
526
|
+
registerTool("certscore_get_latest_domain_scan", toolContract("certscore_get_latest_domain_scan"), async ({ domain, scanFrom }) => {
|
|
264
527
|
try {
|
|
265
528
|
return toToolResult(await client.domains.latest(domain, { scanFrom }));
|
|
266
529
|
}
|
|
@@ -268,9 +531,10 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
268
531
|
return toToolError(error);
|
|
269
532
|
}
|
|
270
533
|
});
|
|
271
|
-
registerTool("
|
|
534
|
+
registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
|
|
272
535
|
try {
|
|
273
|
-
|
|
536
|
+
const guided = withMcpAgentGuidance(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }), "existing_scan_retrieved");
|
|
537
|
+
return toToolResult(guided, preConsentInventoryText(guided));
|
|
274
538
|
}
|
|
275
539
|
catch (error) {
|
|
276
540
|
return toToolError(error);
|