@certscore/mcp 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,8 +1,13 @@
1
+ import { registerAdoptionFeatures, EXAMPLE_SCAN_ID } from "./adoption.js";
2
+ import { withResponseGuidance } from "./response-guidance.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { captureMcpResponse, withResponseCapture } from "./response-capture.js";
5
+ import { z } from "zod";
1
6
  import { captureMcpCallerInput } from "@website-signal-risk-scanner/shared/dist/mcp-caller-input.js";
2
- import { CertScoreClient } from "@certscore/sdk";
3
- import { certScoreMcpToolContracts, isCanonicalScanId } from "@certscore/api-contracts";
7
+ import { CertScoreClient, CertScoreError } from "@certscore/sdk";
8
+ import { certScoreMcpToolContracts, isCanonicalScanId, reportEvidencePageSchema } from "@certscore/api-contracts";
4
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
10
+ import { CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
6
11
  import { sanitizeMcpTaskContext } from "@website-signal-risk-scanner/shared/dist/mcp-product-context.js";
7
12
  import { CERTSCORE_MCP_VERSION } from "./version.js";
8
13
  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";
@@ -202,11 +207,21 @@ export function projectMcpToolInvocationObservation(input) {
202
207
  const error = result.error && typeof result.error === "object" && !Array.isArray(result.error)
203
208
  ? result.error
204
209
  : null;
205
- const errorCode = boundedTelemetryToken(error?.code ?? result.errorCode, 100);
210
+ const completedNoGo = result.status === "completed_limited"
211
+ && result.resultDisposition === "no_go";
212
+ // A completed no-go is a usable terminal scan result. Agent guidance embeds
213
+ // its reason-specific remedy in `error`, but the MCP tool call itself
214
+ // succeeded and must not be counted as a transport or invocation failure.
215
+ const errorCode = completedNoGo
216
+ ? null
217
+ : boundedTelemetryToken(error?.code ?? result.errorCode, 100);
206
218
  const rateLimited = errorCode === "rate_limited" || result.status === "rate_limited";
207
- const isError = Boolean(input.result?.isError) || Boolean(error);
219
+ const isError = !completedNoGo
220
+ && (Boolean(input.result?.isError) || Boolean(error));
208
221
  const outcome = rateLimited ? "rate_limited" : isError ? "error" : "success";
209
- const resultScanId = boundedTelemetryToken(result.scanId ?? result.scan_id ?? result.jobId, 128);
222
+ const scan = result.type === "certscore_domain_latest_scan" && result.scan && typeof result.scan === "object"
223
+ ? result.scan : result;
224
+ const resultScanId = boundedTelemetryToken(scan.scanId ?? scan.scan_id ?? scan.jobId, 128);
210
225
  const inputScanId = boundedTelemetryToken(args.scanId, 128);
211
226
  const requestedResource = requestedTelemetryResource(args);
212
227
  const targetHostname = input.toolName === "certscore_scan_site"
@@ -239,11 +254,12 @@ export function projectMcpToolInvocationObservation(input) {
239
254
  scanDecision,
240
255
  scanFrom: args.scanFrom === "eu_de" || args.scanFrom === "eu_ie" || args.scanFrom === "california"
241
256
  ? args.scanFrom
242
- : result.scanFrom === "eu_de" || result.scanFrom === "eu_ie" || result.scanFrom === "california"
243
- ? result.scanFrom
257
+ : scan.scanFrom === "eu_de" || scan.scanFrom === "eu_ie" || scan.scanFrom === "california"
258
+ ? scan.scanFrom
244
259
  : null,
245
- scanId: resultScanId ?? inputScanId,
246
- scanStatus: boundedTelemetryToken(result.status, 64),
260
+ scanId: outcome === "error" && ["invalid_scan_id", "invalid_arguments", "invalid_url", "unknown_tool"].includes(errorCode ?? "")
261
+ ? null : resultScanId ?? inputScanId,
262
+ scanStatus: boundedTelemetryToken(scan.status ?? scan.scanStatus, 64),
247
263
  targetHostname,
248
264
  toolName: input.toolName,
249
265
  transportOutcome: isError ? "mcp_error" : "mcp_result",
@@ -278,8 +294,8 @@ function observeToolInvocation(observer, observation, requestContext) {
278
294
  });
279
295
  }
280
296
  export function createCertScoreMcpServer(options = {}) {
281
- const createClient = (forwardedClientIp, anonymousRequesterSession) => new CertScoreClient({
282
- apiKey: options.apiKey,
297
+ const createClient = (forwardedClientIp, anonymousRequesterSession, apiKey = options.apiKey) => new CertScoreClient({
298
+ apiKey,
283
299
  baseUrl: options.baseUrl,
284
300
  clientName: "mcp",
285
301
  forwardedClientIp,
@@ -289,24 +305,72 @@ export function createCertScoreMcpServer(options = {}) {
289
305
  timeout: options.timeout
290
306
  });
291
307
  const client = createClient(options.forwardedClientIp, options.resolveAnonymousRequesterSession?.());
292
- const clientForRequest = (extra) => options.resolveForwardedClientIp
293
- ? createClient(options.resolveForwardedClientIp(extra.requestInfo?.headers ?? {}), options.resolveAnonymousRequesterSession?.())
294
- : client;
308
+ const clientForRequest = (extra) => {
309
+ if (!options.resolveApiKey && !options.resolveForwardedClientIp)
310
+ return client;
311
+ const apiKey = options.resolveApiKey ? options.resolveApiKey() : options.apiKey;
312
+ if (options.resolveApiKey && !apiKey?.trim()) {
313
+ throw new Error("Validated MCP request credential is unavailable.");
314
+ }
315
+ return createClient(options.resolveForwardedClientIp ? options.resolveForwardedClientIp(extra.requestInfo?.headers ?? {}) : options.forwardedClientIp, options.resolveAnonymousRequesterSession?.(), apiKey);
316
+ };
295
317
  const server = new McpServer({
296
318
  name: "certscore",
297
319
  version: CERTSCORE_MCP_VERSION
320
+ }, {
321
+ instructions: JSON.stringify({
322
+ setup: {
323
+ route: options.toolProfile === "light" ? "light" : options.grantedOAuthScopes ? "hosted_oauth" : options.anonymousSurface ? "anonymous" : "scoped_api_key",
324
+ scopesGranted: options.grantedOAuthScopes ?? null,
325
+ createAllowedByScope: options.grantedOAuthScopes ? options.grantedOAuthScopes.includes("scan:create") : null,
326
+ resources: options.toolProfile === "light" ? [] : ["certscore://connection", "certscore://project-instructions", "certscore://reconnect", "certscore://example-report"],
327
+ prompts: options.toolProfile === "light" ? [] : ["certscore_launch_review", "certscore_compare_scans", "certscore_remediation_checklist"],
328
+ quotaRemaining: null,
329
+ quotaNote: "Remaining allowance is not loaded at handshake. Scan creation enforces current workspace and requester limits; inspect quota errors rather than assuming a fresh allowance.",
330
+ recommendedNextTool: options.grantedOAuthScopes && !options.grantedOAuthScopes.includes("scan:create") ? "certscore_get_latest_domain_scan" : "certscore_scan_site",
331
+ sequence: ["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle"],
332
+ guidance: options.toolProfile === "light"
333
+ ? "Light supports eligible public scans, not workspace history. For workspace access connect https://mcp.certscore.ai/mcp using OAuth. Reuse an existing Hosted OAuth connection rather than adding duplicate names."
334
+ : "Start a scan, poll only while active at the returned interval, then fetch its bundle. If scan:create is missing, reauthorize with scan:read scan:create mcp. Reuse existing endpoint installations; client names are labels, not verified identities."
335
+ }
336
+ })
298
337
  });
338
+ if (options.toolProfile !== "light")
339
+ registerAdoptionFeatures(server, async () => {
340
+ try {
341
+ return await clientForRequest({}).getConnectionStatus();
342
+ }
343
+ catch (error) {
344
+ if (error instanceof CertScoreError && error.status === 401)
345
+ return { authenticated: false, status: "reconnect_required", quota: null, nextAction: "Read certscore://reconnect and reconnect the existing Hosted OAuth connector. Do not share tokens." };
346
+ return { authenticated: null, status: "check_unavailable", quota: null, nextAction: "Retry the connection check. If your host reports expired or revoked access, read certscore://reconnect. Do not assume a failed check means quota is available." };
347
+ }
348
+ }, async () => {
349
+ try {
350
+ const exampleClient = clientForRequest({});
351
+ const [report, metadata] = await Promise.all([exampleClient.getScanPulse(EXAMPLE_SCAN_ID), exampleClient.getScanResource(EXAMPLE_SCAN_ID)]);
352
+ return { originalCompletedAt: metadata.completedAt ?? null, coverage: metadata.coverage, status: metadata.status, example: true, label: "Retained example, not a current scan of your website", scanId: EXAMPLE_SCAN_ID,
353
+ reportUrl: `https://certscore.ai/scan/${EXAMPLE_SCAN_ID}`,
354
+ report: JSON.stringify(report).length <= 30000 ? report : null,
355
+ note: "Use original timestamps and coverage from the report. If omitted here for size, open the report URL. No new scan was created." };
356
+ }
357
+ catch {
358
+ return { example: true, available: false, scanId: EXAMPLE_SCAN_ID, nextAction: "The retained example is unavailable. Do not create a replacement scan automatically." };
359
+ }
360
+ });
299
361
  const sdkCreateToolError = server.createToolError.bind(server);
300
362
  server.createToolError = (message) => message.includes("Input validation error:")
301
363
  ? toInvalidArgumentsToolError(message)
302
364
  : sdkCreateToolError(message);
303
- const lightTools = new Set(["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle"]);
365
+ const registeredToolNames = new Set();
366
+ const lightTools = new Set(["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle", "certscore_get_report_evidence_page"]);
304
367
  const scanIdTools = new Set([
305
368
  "certscore_explain_finding",
306
369
  "certscore_export_findings",
307
370
  "certscore_get_evidence",
308
371
  "certscore_get_pre_consent_cookies_trackers",
309
372
  "certscore_get_report",
373
+ "certscore_get_report_evidence_page",
310
374
  "certscore_get_scan",
311
375
  "certscore_get_scan_bundle",
312
376
  "certscore_get_scan_status",
@@ -321,17 +385,51 @@ export function createCertScoreMcpServer(options = {}) {
321
385
  return registerRequest(schema, handler);
322
386
  return registerRequest(CallToolRequestSchema, async (request, extra) => {
323
387
  const startedAt = Date.now();
388
+ const requestId = options.resolveRequestId?.() ?? randomUUID();
324
389
  const name = request.params.name;
390
+ if (options.onToolInvocationStarted) {
391
+ void Promise.resolve().then(() => options.onToolInvocationStarted({ requestId, toolName: name, startedAt: new Date(startedAt).toISOString() }))
392
+ .catch(() => console.error("[certscore-mcp] request-start observation failed"));
393
+ }
325
394
  const args = request.params.arguments ?? {};
326
- const known = certScoreMcpToolContracts.some(tool => tool.name === name)
327
- && (options.toolProfile !== "light" || lightTools.has(name));
395
+ const taskContext = sanitizeMcpTaskContext(args.taskContext);
396
+ const forwardedRequest = name === "certscore_scan_site" && args.taskContext !== undefined && !taskContext
397
+ ? {
398
+ ...request,
399
+ params: {
400
+ ...request.params,
401
+ arguments: Object.fromEntries(Object.entries(args).filter(([key]) => key !== "taskContext")),
402
+ },
403
+ }
404
+ : request;
405
+ const known = registeredToolNames.has(name);
328
406
  let result;
407
+ let protocolFailure;
329
408
  try {
330
- result = await handler(request, extra);
409
+ if (!known) {
410
+ result = { isError: true, structuredContent: { error: { code: "unknown_tool" } } };
411
+ const availableTools = [...registeredToolNames];
412
+ const recommendedNextAction = "Refresh the available tools using your MCP client's tool discovery (tools/list), then call a supported tool.";
413
+ throw withResponseCapture(new McpError(ErrorCode.InvalidParams, `This tool is unavailable on this endpoint. ${recommendedNextAction} Available tools: ${availableTools.join(", ")}.`, { code: "unknown_tool", retryable: false, recommendedNextAction, availableTools }), { message: `This tool is unavailable on this endpoint. ${recommendedNextAction} Available tools: ${availableTools.join(", ")}.`, recommendedNextAction });
414
+ }
415
+ const contract = certScoreMcpToolContracts.find(candidate => candidate.name === name);
416
+ const validation = z.object(contract.inputSchema).safeParse(forwardedRequest.params.arguments ?? {});
417
+ result = validation.success
418
+ ? await handler(forwardedRequest, extra)
419
+ : toInvalidArgumentsToolError(`Input validation error: tool ${name}`, {
420
+ tool: name,
421
+ issues: validation.error.issues.map(issue => ({
422
+ // Only schema-owned top-level names; never echo values or dynamic keys.
423
+ field: typeof issue.path[0] === "string" && Object.hasOwn(contract.inputSchema, issue.path[0]) ? issue.path[0] : "arguments",
424
+ code: issue.code,
425
+ ...(issue.code === "invalid_type" && issue.received === "undefined" ? { required: true } : {}),
426
+ })).filter((issue, index, issues) => issues.findIndex(other => other.field === issue.field) === index).slice(0, 8),
427
+ });
331
428
  return result;
332
429
  }
333
430
  catch (error) {
334
- result = { isError: true, structuredContent: { error: { code: "handler_exception" } } };
431
+ protocolFailure = error;
432
+ result ??= { isError: true, structuredContent: { error: { code: "handler_exception" } } };
335
433
  throw error;
336
434
  }
337
435
  finally {
@@ -343,14 +441,16 @@ export function createCertScoreMcpServer(options = {}) {
343
441
  observation.errorCode = "protocol_error";
344
442
  const payload = telemetryResultRecord(result);
345
443
  const metadata = payload.mcpMetadata;
346
- const taskContext = sanitizeMcpTaskContext(args.taskContext);
347
444
  observeToolInvocation(options.onToolInvocation, {
348
445
  ...observation,
446
+ requestId,
447
+ timing: { startedAt: new Date(startedAt).toISOString(), responseGeneratedAt: new Date().toISOString() },
349
448
  captureBasis: "protocol_request",
350
- callerInput: captureMcpCallerInput(args, request.params._meta),
449
+ callerInput: captureMcpCallerInput(args, request.params._meta, { expanded: process.env.MCP_EXPANDED_CALLER_INPUT_ENABLED === "1" }),
351
450
  ...(taskContext ? { taskContext } : {}),
352
451
  response: {
353
- bytes: Math.min(10_000_000, Buffer.byteLength(JSON.stringify(result ?? null))),
452
+ summary: captureMcpResponse(result, protocolFailure),
453
+ bytes: protocolFailure ? null : Math.min(10_000_000, Buffer.byteLength(JSON.stringify(result ?? null))),
354
454
  truncated: typeof metadata?.truncated === "boolean" ? metadata.truncated : null,
355
455
  ...(typeof metadata?.effectiveMaxBytes === "number" ? { effectiveMaxBytes: metadata.effectiveMaxBytes } : {}),
356
456
  },
@@ -372,20 +472,32 @@ export function createCertScoreMcpServer(options = {}) {
372
472
  const scanId = input && typeof input === "object" && !Array.isArray(input)
373
473
  ? input.scanId : null;
374
474
  return scanIdTools.has(name) && !isCanonicalScanId(scanId)
375
- ? toInvalidScanIdToolError() : typedHandler(input, extra);
475
+ ? toInvalidScanIdToolError() : withResponseGuidance(name, input, await typedHandler(input, extra));
376
476
  });
477
+ registeredToolNames.add(name);
377
478
  };
479
+ registerTool("certscore_get_connection_status", toolContract("certscore_get_connection_status"), async (_input, extra) => {
480
+ try {
481
+ const status = await clientForRequest(extra).getConnectionStatus();
482
+ return toToolResult(status, JSON.stringify(status));
483
+ }
484
+ catch (error) {
485
+ return toToolError(error);
486
+ }
487
+ });
378
488
  registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input, extra) => {
379
489
  const toolStartedAtMs = Date.now();
380
490
  const creationStartedAtMs = toolStartedAtMs;
381
491
  const client = clientForRequest(extra);
382
492
  const demoSubstitution = exampleDomainDemoSubstitution(input.url, options.exampleDomainDemoUrl);
383
493
  const effectiveUrl = demoSubstitution?.effectiveUrl ?? input.url;
494
+ let creationCompleted = false;
384
495
  try {
385
496
  const created = await client.scans.create(effectiveUrl, {
386
497
  freshness: input.freshness ?? "latest",
387
498
  scanFrom: input.scanFrom
388
499
  });
500
+ creationCompleted = true;
389
501
  console.log(JSON.stringify({
390
502
  event: "mcp.certscore_scan_site.creation_completed",
391
503
  durationMs: Date.now() - creationStartedAtMs,
@@ -394,6 +506,8 @@ export function createCertScoreMcpServer(options = {}) {
394
506
  reused: created.reused === true,
395
507
  status: created.status ?? null,
396
508
  }));
509
+ let retainedPreviewWaitMs = 0;
510
+ let retainedInternalReadCount = 0;
397
511
  let initialResult = created;
398
512
  const stableScanId = typeof created.scanId === "string" && created.scanId
399
513
  ? created.scanId
@@ -445,6 +559,8 @@ export function createCertScoreMcpServer(options = {}) {
445
559
  scanId: stableScanId,
446
560
  }));
447
561
  }
562
+ retainedPreviewWaitMs = Date.now() - previewWaitStartedAtMs;
563
+ retainedInternalReadCount = internalReadCount;
448
564
  console.log(JSON.stringify({
449
565
  event: "mcp.certscore_scan_site.preview_wait_completed",
450
566
  durationMs: Date.now() - previewWaitStartedAtMs,
@@ -456,7 +572,11 @@ export function createCertScoreMcpServer(options = {}) {
456
572
  }));
457
573
  }
458
574
  const guided = withExampleDomainDemo(withMcpAgentGuidance(initialResult, "unknown", "scan_creation"), demoSubstitution);
459
- return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
575
+ const toolResult = toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
576
+ return options.toolProfile === "light" ? withResponseCapture(toolResult, {
577
+ firstResult: ["completed", "completed_limited"].includes(initialResult.status) ? "completed" : ["failed", "expired"].includes(initialResult.status) ? "failed" : hasPreConsentPreview(initialResult) ? "preview" : activeScan(initialResult) ? "queued" : "unknown",
578
+ previewWaitMs: retainedPreviewWaitMs, internalReadCount: retainedInternalReadCount,
579
+ }) : toolResult;
460
580
  }
461
581
  catch (error) {
462
582
  console.warn(JSON.stringify({
@@ -464,10 +584,11 @@ export function createCertScoreMcpServer(options = {}) {
464
584
  durationMs: Date.now() - creationStartedAtMs,
465
585
  errorName: error instanceof Error ? error.name : "UnknownError",
466
586
  }));
467
- return toToolError(error);
587
+ return toToolError(error, { scanCreation: !creationCompleted });
468
588
  }
469
589
  });
470
- registerTool("certscore_get_scan", toolContract("certscore_get_scan"), async ({ scanId }) => {
590
+ registerTool("certscore_get_scan", toolContract("certscore_get_scan"), async ({ scanId }, extra) => {
591
+ const client = clientForRequest(extra);
471
592
  try {
472
593
  return toToolResult(await client.scans.get(scanId));
473
594
  }
@@ -491,7 +612,8 @@ export function createCertScoreMcpServer(options = {}) {
491
612
  return toToolError(error);
492
613
  }
493
614
  });
494
- registerTool("certscore_get_report", toolContract("certscore_get_report"), async ({ scanId, detail, format }) => {
615
+ registerTool("certscore_get_report", toolContract("certscore_get_report"), async ({ scanId, detail, format }, extra) => {
616
+ const client = clientForRequest(extra);
495
617
  try {
496
618
  const normalizedFormat = normalizeFormat(format);
497
619
  const result = normalizedFormat === "markdown"
@@ -518,7 +640,8 @@ export function createCertScoreMcpServer(options = {}) {
518
640
  return toToolError(error);
519
641
  }
520
642
  });
521
- registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }) => {
643
+ registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }, extra) => {
644
+ const client = clientForRequest(extra);
522
645
  try {
523
646
  const bounded = boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" }), MAX_EVIDENCE_PACKET_CHARS - 2_500);
524
647
  const guided = withMcpAgentGuidance(bounded, "existing_scan_retrieved");
@@ -528,6 +651,15 @@ export function createCertScoreMcpServer(options = {}) {
528
651
  return toToolError(error);
529
652
  }
530
653
  });
654
+ registerTool("certscore_get_report_evidence_page", toolContract("certscore_get_report_evidence_page"), async ({ scanId, cursor }, extra) => {
655
+ try {
656
+ const page = reportEvidencePageSchema.parse(await clientForRequest(extra).getReportEvidencePage(scanId, { cursor, timeout: 30_000, internalMcpOperation: { operation: "scan_bundle", scanId } }));
657
+ return toToolResult(page, `Report evidence for ${scanId}: ${page.pagination.offset + 1}–${page.pagination.offset + page.pagination.returned} of ${page.pagination.total} entries. ${page.pagination.complete ? "Export complete; preserve report coverage limitations." : `Continue with certscore_get_report_evidence_page using scanId and cursor ${page.pagination.nextCursor}.`} Evidence values are in structuredContent.entries. ${page.download ? `Full report JSON: ${page.download.url} (${page.download.bytes} bytes). ${page.download.instructions}` : ""} ${page.reportUrl}`);
658
+ }
659
+ catch (error) {
660
+ return toToolError(error);
661
+ }
662
+ });
531
663
  registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }, extra) => {
532
664
  const client = clientForRequest(extra);
533
665
  try {
@@ -553,7 +685,7 @@ export function createCertScoreMcpServer(options = {}) {
553
685
  responseCeilingBytes,
554
686
  scan
555
687
  });
556
- return toToolResult(bundle, scanBundleText(bundle));
688
+ return toToolResult(bundle, scanBundleText(bundle, { lightTrialCta: options.toolProfile === "light" }));
557
689
  }
558
690
  const includeEvidence = detail === "evidence" || detail === "full";
559
691
  const reportDetail = detail === "full" ? "full" : includeEvidence ? "evidence" : "summary";
@@ -578,13 +710,14 @@ export function createCertScoreMcpServer(options = {}) {
578
710
  responseCeilingBytes,
579
711
  scan
580
712
  });
581
- return toToolResult(bundle, scanBundleText(bundle));
713
+ return toToolResult(bundle, scanBundleText(bundle, { lightTrialCta: options.toolProfile === "light" }));
582
714
  }
583
715
  catch (error) {
584
716
  return toToolError(error);
585
717
  }
586
718
  });
587
- registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }) => {
719
+ registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }, extra) => {
720
+ const client = clientForRequest(extra);
588
721
  try {
589
722
  const report = await client.getScan(scanId, { detail: "full", format: "json" });
590
723
  const guided = withMcpAgentGuidance(exportFindings(report), "existing_scan_retrieved");
@@ -594,7 +727,8 @@ export function createCertScoreMcpServer(options = {}) {
594
727
  return toToolError(error);
595
728
  }
596
729
  });
597
- registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }) => {
730
+ registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }, extra) => {
731
+ const client = clientForRequest(extra);
598
732
  try {
599
733
  const guided = withMcpAgentGuidance(paginateFindingList(await client.findings.list(scanId), { limit, offset }), "existing_scan_retrieved");
600
734
  return toToolResult(guided, findingListText(guided));
@@ -603,7 +737,8 @@ export function createCertScoreMcpServer(options = {}) {
603
737
  return toToolError(error);
604
738
  }
605
739
  });
606
- registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
740
+ registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }, extra) => {
741
+ const client = clientForRequest(extra);
607
742
  try {
608
743
  const guided = withMcpAgentGuidance(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }), "existing_scan_retrieved");
609
744
  return toToolResult(guided, preConsentInventoryText(guided));
@@ -612,7 +747,8 @@ export function createCertScoreMcpServer(options = {}) {
612
747
  return toToolError(error);
613
748
  }
614
749
  });
615
- registerTool("certscore_explain_finding", toolContract("certscore_explain_finding"), async ({ scanId, findingId }) => {
750
+ registerTool("certscore_explain_finding", toolContract("certscore_explain_finding"), async ({ scanId, findingId }, extra) => {
751
+ const client = clientForRequest(extra);
616
752
  try {
617
753
  return toToolResult(await client.findings.explain(scanId, findingId));
618
754
  }
@@ -620,7 +756,8 @@ export function createCertScoreMcpServer(options = {}) {
620
756
  return toToolError(error);
621
757
  }
622
758
  });
623
- registerTool("certscore_get_latest_domain_scan", toolContract("certscore_get_latest_domain_scan"), async ({ domain, scanFrom }) => {
759
+ registerTool("certscore_get_latest_domain_scan", toolContract("certscore_get_latest_domain_scan"), async ({ domain, scanFrom }, extra) => {
760
+ const client = clientForRequest(extra);
624
761
  try {
625
762
  return toToolResult(await client.domains.latest(domain, { scanFrom }));
626
763
  }
@@ -628,7 +765,8 @@ export function createCertScoreMcpServer(options = {}) {
628
765
  return toToolError(error);
629
766
  }
630
767
  });
631
- registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
768
+ registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }, extra) => {
769
+ const client = clientForRequest(extra);
632
770
  try {
633
771
  const guided = withMcpAgentGuidance(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }), "existing_scan_retrieved");
634
772
  return toToolResult(guided, preConsentInventoryText(guided));
@@ -638,5 +776,12 @@ export function createCertScoreMcpServer(options = {}) {
638
776
  }
639
777
  });
640
778
  server.server.setRequestHandler = registerRequest;
779
+ // Reconnecting hosts may retain a tool catalog from an older release.
780
+ // Announce the current catalog once the new session has initialized.
781
+ server.server.oninitialized = () => {
782
+ void server.server.sendToolListChanged().catch(() => {
783
+ // A host without a notification channel can still call tools/list normally.
784
+ });
785
+ };
641
786
  return server;
642
787
  }
package/dist/tools.d.ts CHANGED
@@ -13,8 +13,17 @@ type ActionableError = {
13
13
  mcpCode?: number;
14
14
  };
15
15
  export declare function toToolResult(payload: unknown, text?: string): CallToolResult;
16
- export declare function toToolError(error: unknown): CallToolResult;
17
- export declare function toInvalidArgumentsToolError(errorMessage: string): CallToolResult;
16
+ export declare function toToolError(error: unknown, context?: {
17
+ scanCreation?: boolean;
18
+ }): CallToolResult;
19
+ export declare function toInvalidArgumentsToolError(errorMessage: string, validation?: {
20
+ tool: string;
21
+ issues: {
22
+ field: string;
23
+ code: string;
24
+ required?: boolean;
25
+ }[];
26
+ }): CallToolResult;
18
27
  export declare function toInvalidScanIdToolError(): CallToolResult;
19
28
  export declare function withMcpAgentGuidance<T extends Record<string, any>>(value: T, fallbackProvenanceMode?: ScanProvenanceMode, pollGuidanceContext?: McpPollGuidanceContext): T & {
20
29
  error: ActionableError | null;
@@ -39,6 +48,10 @@ export declare function scanIdFromPulse(report: PulseResult): string | null;
39
48
  export declare function findingsFromReport(report: PulseResult): TopFinding[];
40
49
  export declare function exportFindings(report: PulseResult): {
41
50
  type: string;
51
+ exportVersion: string;
52
+ exportedAt: string;
53
+ returnedFindingCount: number;
54
+ completeness: string;
42
55
  scanId: string | null;
43
56
  domain: string | null;
44
57
  summary: import("packages/certscore-sdk/dist/types.js").PulseSummary | null;
@@ -71,7 +84,9 @@ export declare function findingListText(value: Record<string, any>, label?: stri
71
84
  export declare function preConsentInventoryText(value: Record<string, any>): string;
72
85
  export declare function pulseReportText(value: Record<string, any>, label?: string): string;
73
86
  export declare function markdownReportText(value: Record<string, any>): string;
74
- export declare function scanBundleText(bundle: Record<string, any>): string;
87
+ export declare function scanBundleText(bundle: Record<string, any>, options?: {
88
+ lightTrialCta?: boolean;
89
+ }): string;
75
90
  export declare function buildScanBundle(input: {
76
91
  detail?: "summary" | "findings" | "evidence" | "full";
77
92
  evidence?: PulseResult | null;
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}