@certscore/mcp 0.2.15 → 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/dist/index.js CHANGED
@@ -141,7 +141,7 @@ export async function getCertScoreMcpDoctorReport(options = {}) {
141
141
  exitCode = 1;
142
142
  }
143
143
  }
144
- lines.push("CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.");
144
+ lines.push("CertScore outputs are automated public-web observations for human and agentic review, not legal advice, certification, or a compliance determination.");
145
145
  return { exitCode, lines };
146
146
  }
147
147
  function isMainModule() {
package/dist/server.d.ts CHANGED
@@ -1,11 +1,53 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { RequestInfo } from "@modelcontextprotocol/sdk/types.js";
2
3
  export interface CertScoreMcpOptions {
3
4
  apiKey?: string;
4
5
  baseUrl?: string;
5
6
  forwardedClientIp?: string | null;
7
+ resolveForwardedClientIp?: (headers: RequestInfo["headers"]) => string | null;
8
+ resolveAnonymousRequesterSession?: () => string | null;
6
9
  anonymousRequesterSecret?: string | null;
10
+ anonymousSurface?: "mcp_light" | "mcp_anonymous" | null;
7
11
  timeout?: number;
8
12
  toolProfile?: "full" | "light";
13
+ exampleDomainDemoUrl?: string | null;
14
+ onToolInvocation?: (observation: McpToolInvocationObservation, requestContext: McpToolInvocationRequestContext) => void | Promise<void>;
9
15
  }
16
+ export type McpToolInvocationRequestContext = {
17
+ headers: RequestInfo["headers"] | null;
18
+ };
19
+ export type McpToolInvocationObservation = {
20
+ durationMs: number;
21
+ errorCode: string | null;
22
+ freshness: "latest" | "refresh" | null;
23
+ isCanary: boolean;
24
+ outcome: "success" | "error" | "rate_limited";
25
+ quotaOutcome: "allowed" | "rate_limited";
26
+ requestedResource: string | null;
27
+ requestedResourceType: "url" | "domain" | "scan_id" | "job_id" | null;
28
+ scanDecision: "reused" | "new" | "unavailable" | "not_applicable";
29
+ scanFrom: "eu_de" | "eu_ie" | "california" | null;
30
+ scanId: string | null;
31
+ scanStatus: string | null;
32
+ targetHostname: string | null;
33
+ toolName: string;
34
+ transportOutcome: "mcp_result" | "mcp_error";
35
+ };
36
+ export declare function resolveMcpScanSiteWaitBudget(input: {
37
+ maxWaitSeconds?: number;
38
+ startedAtMs: number;
39
+ nowMs: number;
40
+ }): {
41
+ elapsedMs: number;
42
+ remainingWaitMs: number;
43
+ responseReserveMs: number;
44
+ totalBudgetMs: number;
45
+ };
46
+ export declare function projectMcpToolInvocationObservation(input: {
47
+ args: unknown;
48
+ durationMs: number;
49
+ result: unknown;
50
+ toolName: string;
51
+ }): McpToolInvocationObservation;
10
52
  export declare function createCertScoreMcpServer(options?: CertScoreMcpOptions): McpServer;
11
53
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC;AAyED,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,mBAAwB,aAkSzE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAItE,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,KAAK,MAAM,GAAG,IAAI,CAAC;IAC9E,gCAAgC,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACvD,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,gBAAgB,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,gBAAgB,CAAC,EAAE,CACjB,WAAW,EAAE,4BAA4B,EACzC,cAAc,EAAE,+BAA+B,KAC5C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAID,MAAM,MAAM,+BAA+B,GAAG;IAC5C,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;CACxC,CAAC;AA4BF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;IAC9C,YAAY,EAAE,SAAS,GAAG,cAAc,CAAC;IACzC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,qBAAqB,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;IACtE,YAAY,EAAE,QAAQ,GAAG,KAAK,GAAG,aAAa,GAAG,gBAAgB,CAAC;IAClE,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,YAAY,GAAG,IAAI,CAAC;IAClD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,YAAY,GAAG,WAAW,CAAC;CAC9C,CAAC;AAOF,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;;;;;EAYA;AAoJD,wBAAgB,mCAAmC,CAAC,KAAK,EAAE;IACzD,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,4BAA4B,CAqD/B;AAkBD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,mBAAwB,aA8XzE"}
package/dist/server.js CHANGED
@@ -1,9 +1,52 @@
1
- import { CertScoreClient, CertScoreTimeoutError } from "@certscore/sdk";
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 DEFAULT_MCP_SCAN_WAIT_MS = 45_000;
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
+ }
7
50
  async function retryTransientOriginFailure(operation) {
8
51
  try {
9
52
  return await operation();
@@ -43,15 +86,161 @@ function toolContract(name) {
43
86
  annotations: contract.annotations
44
87
  };
45
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
+ }
46
229
  export function createCertScoreMcpServer(options = {}) {
47
- const client = new CertScoreClient({
230
+ const createClient = (forwardedClientIp, anonymousRequesterSession) => new CertScoreClient({
48
231
  apiKey: options.apiKey,
49
232
  baseUrl: options.baseUrl,
50
233
  clientName: "mcp",
51
- forwardedClientIp: options.forwardedClientIp,
234
+ forwardedClientIp,
52
235
  anonymousRequesterSecret: options.anonymousRequesterSecret,
236
+ anonymousSurface: options.anonymousSurface,
237
+ anonymousRequesterSession,
53
238
  timeout: options.timeout
54
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;
55
244
  const server = new McpServer({
56
245
  name: "certscore",
57
246
  version: CERTSCORE_MCP_VERSION
@@ -61,30 +250,100 @@ export function createCertScoreMcpServer(options = {}) {
61
250
  ? toInvalidArgumentsToolError(message)
62
251
  : sdkCreateToolError(message);
63
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
+ ]);
64
264
  const registerMcpTool = server.registerTool.bind(server);
65
265
  const registerTool = (name, contract, handler) => {
66
266
  if (options.toolProfile === "light" && !lightTools.has(name)) {
67
267
  return;
68
268
  }
69
- registerMcpTool(name, contract, handler);
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;
286
+ }
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
+ });
70
302
  };
71
- registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input) => {
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;
72
308
  try {
73
- const created = await client.scans.create(input.url, {
309
+ const created = await client.scans.create(effectiveUrl, {
74
310
  freshness: input.freshness ?? "latest",
75
311
  scanFrom: input.scanFrom
76
312
  });
77
313
  if (input.waitForCompletion === false || created.type === "certscore_scan") {
78
- return toToolResult(withMcpAgentGuidance(created));
314
+ const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
315
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
79
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);
80
335
  try {
336
+ const internalMcpOperation = { operation: "scan_site_wait", scanId: created.scanId ?? created.scan_id ?? created.jobId };
81
337
  const completed = await client.scans.wait(created, {
82
- maxWaitMs: Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_WAIT_MS, DEFAULT_MCP_SCAN_WAIT_MS)
338
+ maxWaitMs: waitBudget.remainingWaitMs,
339
+ internalMcpOperation,
340
+ signal: waitAbortController.signal,
83
341
  });
84
- return toToolResult(withMcpAgentGuidance({
342
+ const guided = withExampleDomainDemo(withMcpAgentGuidance({
85
343
  ...completed,
86
344
  ...scanCreationMetadata(created)
87
- }));
345
+ }), demoSubstitution);
346
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
88
347
  }
89
348
  catch (error) {
90
349
  const scanId = created.scanId ?? created.scan_id;
@@ -93,25 +352,18 @@ export function createCertScoreMcpServer(options = {}) {
93
352
  errorName: error instanceof Error ? error.name : "UnknownError",
94
353
  jobId: created.jobId ?? null,
95
354
  scanId: scanId ?? null,
96
- status: created.status ?? null
355
+ status: created.status ?? null,
356
+ ...waitBudget,
97
357
  }));
98
- if (error instanceof CertScoreTimeoutError && scanId) {
99
- try {
100
- return toToolResult(withMcpAgentGuidance({
101
- ...(await client.scans.status(scanId)),
102
- ...scanCreationMetadata(created)
103
- }));
104
- }
105
- catch {
106
- // Creation already succeeded. Preserve that stable identity when
107
- // the follow-up status read is briefly unavailable.
108
- }
109
- }
110
358
  // Waiting is a convenience layered on top of scan creation. Once the
111
359
  // API has accepted a scan, never turn a transient polling or hydration
112
360
  // failure into an identity-less tool error that encourages callers to
113
361
  // submit a second, non-idempotent certscore_scan_site request.
114
- return toToolResult(withMcpAgentGuidance(created));
362
+ const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
363
+ return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
364
+ }
365
+ finally {
366
+ clearTimeout(waitAbortTimer);
115
367
  }
116
368
  }
117
369
  catch (error) {
@@ -126,41 +378,17 @@ export function createCertScoreMcpServer(options = {}) {
126
378
  return toToolError(error);
127
379
  }
128
380
  });
129
- registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }) => {
381
+ registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }, extra) => {
382
+ const client = clientForRequest(extra);
130
383
  try {
131
- const status = await client.scans.status(scanId);
132
- const needsTerminalHydration = status.status === "completed" || status.status === "completed_limited";
133
- if (needsTerminalHydration) {
134
- try {
135
- const scan = await client.scans.get(scanId);
136
- return toToolResult(withMcpAgentGuidance({
137
- ...status,
138
- type: "certscore_scan_job",
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));
384
+ const internalMcpOperation = { operation: "scan_status", scanId };
385
+ const status = await client.scans.status(scanId, { internalMcpOperation });
386
+ const guided = withMcpScanProvenanceGuidance({
387
+ ...status,
388
+ jobId: undefined,
389
+ scanFrom: status.scanFrom ?? null
390
+ }, "existing_scan_retrieved");
391
+ return toToolResult(guided, scanStatusText(guided));
164
392
  }
165
393
  catch (error) {
166
394
  return toToolError(error);
@@ -178,7 +406,16 @@ export function createCertScoreMcpServer(options = {}) {
178
406
  detail: normalizeDetail(detail),
179
407
  format: "json"
180
408
  });
181
- return toToolResult(result);
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));
182
419
  }
183
420
  catch (error) {
184
421
  return toToolError(error);
@@ -186,49 +423,65 @@ export function createCertScoreMcpServer(options = {}) {
186
423
  });
187
424
  registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }) => {
188
425
  try {
189
- return toToolResult(boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" })));
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"));
190
429
  }
191
430
  catch (error) {
192
431
  return toToolError(error);
193
432
  }
194
433
  });
195
- registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }) => {
434
+ registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }, extra) => {
435
+ const client = clientForRequest(extra);
196
436
  try {
197
- const scan = await retryTransientOriginFailure(() => client.scans.get(scanId));
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 }));
198
445
  if (scan.status === "completed_limited" && scan.resultDisposition === "no_go") {
199
- return toToolResult(buildScanBundle({
446
+ const bundle = buildScanBundle({
200
447
  detail,
201
448
  evidence: null,
202
449
  findings: { type: "certscore_finding_list", scanId, findings: [] },
203
- maxBytes,
450
+ maxBytes: requestedMaxBytes,
204
451
  maxFindings,
205
452
  maxPreConsentRows,
206
453
  preConsentCookiesTrackers: null,
207
454
  report: null,
455
+ requestedMaxBytes,
456
+ responseCeilingBytes,
208
457
  scan
209
- }));
458
+ });
459
+ return toToolResult(bundle, scanBundleText(bundle));
210
460
  }
211
461
  const includeEvidence = detail === "evidence" || detail === "full";
212
462
  const reportDetail = detail === "full" ? "full" : includeEvidence ? "evidence" : "summary";
213
463
  const [report, findings, preConsentCookiesTrackers] = await Promise.all([
214
- retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json" })),
215
- retryTransientOriginFailure(() => client.findings.list(scanId)),
216
- includeEvidence
217
- ? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId))
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 }))
218
468
  : Promise.resolve(null)
219
469
  ]);
220
470
  const evidence = includeEvidence ? report : null;
221
- return toToolResult(buildScanBundle({
471
+ const bundle = buildScanBundle({
222
472
  detail,
223
473
  evidence,
224
474
  findings,
225
- maxBytes,
475
+ maxBytes: requestedMaxBytes,
226
476
  maxFindings,
227
477
  maxPreConsentRows,
228
478
  preConsentCookiesTrackers,
229
479
  report,
480
+ requestedMaxBytes,
481
+ responseCeilingBytes,
230
482
  scan
231
- }));
483
+ });
484
+ return toToolResult(bundle, scanBundleText(bundle));
232
485
  }
233
486
  catch (error) {
234
487
  return toToolError(error);
@@ -237,7 +490,8 @@ export function createCertScoreMcpServer(options = {}) {
237
490
  registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }) => {
238
491
  try {
239
492
  const report = await client.getScan(scanId, { detail: "full", format: "json" });
240
- return toToolResult(exportFindings(report));
493
+ const guided = withMcpAgentGuidance(exportFindings(report), "existing_scan_retrieved");
494
+ return toToolResult(guided, findingListText(guided, "Exported canonical projected findings"));
241
495
  }
242
496
  catch (error) {
243
497
  return toToolError(error);
@@ -245,7 +499,8 @@ export function createCertScoreMcpServer(options = {}) {
245
499
  });
246
500
  registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }) => {
247
501
  try {
248
- return toToolResult(paginateFindingList(await client.findings.list(scanId), { limit, offset }));
502
+ const guided = withMcpAgentGuidance(paginateFindingList(await client.findings.list(scanId), { limit, offset }), "existing_scan_retrieved");
503
+ return toToolResult(guided, findingListText(guided));
249
504
  }
250
505
  catch (error) {
251
506
  return toToolError(error);
@@ -253,7 +508,8 @@ export function createCertScoreMcpServer(options = {}) {
253
508
  });
254
509
  registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
255
510
  try {
256
- return toToolResult(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }));
511
+ const guided = withMcpAgentGuidance(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }), "existing_scan_retrieved");
512
+ return toToolResult(guided, preConsentInventoryText(guided));
257
513
  }
258
514
  catch (error) {
259
515
  return toToolError(error);
@@ -277,7 +533,8 @@ export function createCertScoreMcpServer(options = {}) {
277
533
  });
278
534
  registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
279
535
  try {
280
- return toToolResult(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }));
536
+ const guided = withMcpAgentGuidance(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }), "existing_scan_retrieved");
537
+ return toToolResult(guided, preConsentInventoryText(guided));
281
538
  }
282
539
  catch (error) {
283
540
  return toToolError(error);