@certscore/mcp 0.2.6 → 0.2.11

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CertScore MCP exposes a focused Model Context Protocol server for CertScore Pulse workflows.
4
4
 
5
- Status: public developer preview. Version 0.2.6 is available as a Homebrew/npm stdio server and as a hosted OAuth-protected Streamable HTTP service. Local WC01 development uses `pnpm mcp:certscore`.
5
+ Status: public developer preview. Version 0.2.11 is prepared for the hosted Streamable HTTP service and the next deterministic Homebrew release. Local WC01 development uses `pnpm mcp:certscore`.
6
6
 
7
7
  Public docs:
8
8
 
@@ -14,11 +14,12 @@ Public docs:
14
14
  ## Tools
15
15
 
16
16
  - `create_scan` - Deprecated compatibility alias of scan_site. Use scan_site for new integrations. Returns completed-limited no-go disposition and reason-specific guidance when applicable.
17
- - `scan_site` - Start or reuse a CertScore public-web scan. Completed no-go scans return completed_limited status, structured reason-specific guidance, and timing when available.
17
+ - `scan_site` - Recommended first call. Starts or reuses a public-web scan, reports freshness and anonymous quota decisions, and waits up to 45 seconds by default. If still running, use get_scan_status; otherwise use get_scan_bundle. Completed no-go scans retain completed_limited status and reason-specific guidance.
18
18
  - `get_scan` - Retrieve the API v2 public-safe scan resource, including completed-limited no-go disposition, reason-specific guidance, and timing when available.
19
19
  - `get_scan_status` - Retrieve terminal status, including completed_limited no-go disposition and reason-specific guidance. Pass jobId only before a stable scanId is available.
20
20
  - `get_report` - Retrieve a summary Pulse report, including customer-safe no-go messaging when coverage is completed-limited. Use get_evidence for the larger bounded packet.
21
21
  - `get_evidence` - Retrieve the bounded structured Evidence JSON packet for a stable scan ID. Excludes raw cookie values, raw bodies, sensitive payloads, full DOM, and unredacted query values.
22
+ - `get_scan_bundle` - Recommended second call after scan_site. Returns the canonical scan state, compact report summary, findings, bounded evidence summary, and pre-consent inventory in one agent-friendly response.
22
23
  - `export_findings` - Return structured findings plus completed-limited no-go disposition and guidance for downstream review or ticketing workflows.
23
24
  - `list_findings` - List API v2 public-safe findings already projected for a scan.
24
25
  - `get_pre_consent_cookies_trackers` - Retrieve the public-safe Cookies & Trackers (Pre-consent) report table as compact JSON for a scan.
@@ -59,6 +60,15 @@ https://certscore.ai/.well-known/oauth-authorization-server
59
60
 
60
61
  The hosted service uses OAuth authorization code with PKCE. Default read access requests `scan:read mcp`; support-gated scan creation additionally requests `scan:create`. The same tool implementation and output contracts power stdio and hosted transports.
61
62
 
63
+ For low-volume agent discovery without account or OAuth setup, use the unauthenticated endpoint:
64
+
65
+ ```text
66
+ https://mcp.certscore.ai/mcp/anonymous
67
+ ```
68
+
69
+ It exposes the same public-safe tool surface and preserves the existing limit of 10 new scans per requester IP per UTC day. Reusing an
70
+ eligible recent result does not consume the quota. Use the OAuth endpoint or a scoped API key for higher-volume workflows.
71
+
62
72
  ## Configuration
63
73
 
64
74
  Install with Homebrew on macOS:
@@ -124,9 +134,22 @@ Stdio API keys use `pulse:read` and `mcp`; creating scans additionally requires
124
134
  certscore-mcp --version
125
135
  certscore-mcp --help
126
136
  CERTSCORE_API_KEY=... certscore-mcp doctor
137
+ CERTSCORE_API_KEY=... certscore-mcp doctor --check-auth
127
138
  ```
128
139
 
129
- The doctor command checks binary startup, version output, Node.js runtime compatibility, the configured CertScore base URL, API v2 health, and API key presence. It does not print secrets, create scans, or inspect raw scanner artifacts. There is no dedicated public auth-check endpoint; verify credentials with a real MCP tool call such as `scan_site` after the client is connected.
140
+ The doctor command checks binary startup, version output, Node.js runtime compatibility, the configured CertScore base URL, API v2 health, and API key presence. Add `--check-auth` to validate the credential against `/api/v2/auth/check` without creating a scan. It does not print secrets or inspect raw scanner artifacts.
141
+
142
+ ## No-account agent scan path
143
+
144
+ Agents that cannot create an account or configure OAuth can use the public API v2 scan path without an `Authorization` header:
145
+
146
+ ```bash
147
+ curl -X POST https://certscore.ai/api/v2/scans \
148
+ -H "Content-Type: application/json" \
149
+ -d '{"url":"https://example.com","freshness":"latest","scanFrom":"eu_ie"}'
150
+ ```
151
+
152
+ New anonymous scans are limited to 10 per requester IP per UTC day. Reusing an eligible recent result does not consume the quota. Poll the returned status resource, then retrieve findings or evidence. Use OAuth or a scoped API key for repeated or higher-volume workflows.
130
153
 
131
154
  ## MCP Client Examples
132
155
 
@@ -198,14 +221,16 @@ Local repo config for contributors:
198
221
 
199
222
  ## Agent Workflow
200
223
 
201
- 1. Call `scan_site` with a public URL.
202
- 2. If it returns a `jobId`, call `get_scan_status` until the scan completes.
203
- 3. Call `get_scan` with the stable `scanId`.
204
- 4. Call `list_findings` to route structured findings into review workflows.
205
- 5. Call `get_evidence` when a reviewer or agent needs the larger bounded evidence packet.
206
- 6. Call `get_pre_consent_cookies_trackers` when the user asks for Cookies & Trackers (Pre-consent) table data as JSON.
207
- 7. Call `explain_finding` when a reviewer needs evidence and caveats for a specific finding.
208
- 8. Call `get_latest_domain_scan` or `get_latest_domain_pre_consent_cookies_trackers` when the user asks for latest eligible public data for a domain.
224
+ 1. Call `scan_site` with a public URL. It normally returns the completed scan resource in the same tool call.
225
+ 2. Only if it returns a non-terminal job, call `get_scan_status` using the stable `scanId` until completion.
226
+ 3. Call `get_scan_bundle` for the normal compact review handoff.
227
+ 4. Call `get_report`, `get_evidence`, `list_findings`, or `get_pre_consent_cookies_trackers` only when the task needs a dedicated view.
228
+ 5. Call `explain_finding` when a reviewer needs evidence and caveats for a specific finding.
229
+ 6. Call `get_latest_domain_scan` or `get_latest_domain_pre_consent_cookies_trackers` when the user asks for latest eligible public data for a domain.
230
+
231
+ `scan_site` reports whether the result was reused, why the freshness decision was made, whether anonymous quota was consumed, the remaining daily allowance, the UTC reset time, and the recommended next tool.
232
+
233
+ With `freshness: "latest"`, CertScore reuses an eligible scan completed within the last 24 hours for the same normalized target and scan region. A reusable result must have completed usable page coverage and must not be an early-loss, no-page, or otherwise non-reusable limited result. Reuse does not consume anonymous quota. `freshnessDecision` states whether a recent result was reused or a new scan was queued; `reusedScanAgeSeconds` reports the reused result's age.
209
234
 
210
235
  ```json
211
236
  {
@@ -15386,6 +15386,9 @@ var StdioServerTransport = class {
15386
15386
  import { realpathSync } from "node:fs";
15387
15387
  import { fileURLToPath } from "node:url";
15388
15388
 
15389
+ // ../certscore-sdk/dist/client.js
15390
+ import { createHmac } from "node:crypto";
15391
+
15389
15392
  // ../certscore-sdk/dist/errors.js
15390
15393
  var CertScoreError = class extends Error {
15391
15394
  status;
@@ -15587,6 +15590,9 @@ function isStatus(body) {
15587
15590
  var CertScoreClient = class {
15588
15591
  apiKey;
15589
15592
  baseUrl;
15593
+ clientName;
15594
+ forwardedClientIp;
15595
+ anonymousRequesterSecret;
15590
15596
  timeout;
15591
15597
  scans;
15592
15598
  findings;
@@ -15596,6 +15602,9 @@ var CertScoreClient = class {
15596
15602
  constructor(options = {}) {
15597
15603
  this.apiKey = options.apiKey;
15598
15604
  this.baseUrl = normalizeBaseUrl(options.baseUrl);
15605
+ this.clientName = options.clientName ?? "sdk";
15606
+ this.forwardedClientIp = options.forwardedClientIp?.trim() || void 0;
15607
+ this.anonymousRequesterSecret = options.anonymousRequesterSecret?.trim() || void 0;
15599
15608
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
15600
15609
  this.scans = {
15601
15610
  create: (url2, scanOptions) => this.createScanResource(url2, scanOptions),
@@ -16002,7 +16011,8 @@ var CertScoreClient = class {
16002
16011
  }
16003
16012
  headers(jsonBody = false) {
16004
16013
  const headers = {
16005
- Accept: "application/json, text/markdown;q=0.9"
16014
+ Accept: "application/json, text/markdown;q=0.9",
16015
+ "X-CertScore-Client": this.clientName
16006
16016
  };
16007
16017
  if (jsonBody) {
16008
16018
  headers["Content-Type"] = "application/json; charset=utf-8";
@@ -16010,6 +16020,17 @@ var CertScoreClient = class {
16010
16020
  if (this.apiKey) {
16011
16021
  headers.Authorization = `Bearer ${this.apiKey}`;
16012
16022
  }
16023
+ if (this.forwardedClientIp && !this.apiKey) {
16024
+ headers["X-Forwarded-For"] = this.forwardedClientIp;
16025
+ if (this.anonymousRequesterSecret) {
16026
+ const timestamp = String(Math.floor(Date.now() / 1e3));
16027
+ const message = `${timestamp}.${this.forwardedClientIp}`;
16028
+ const proof = createHmac("sha256", this.anonymousRequesterSecret).update(message).digest("base64url");
16029
+ headers["X-CertScore-Anonymous-Requester-IP"] = this.forwardedClientIp;
16030
+ headers["X-CertScore-Anonymous-Requester-Timestamp"] = timestamp;
16031
+ headers["X-CertScore-Anonymous-Requester-Proof"] = proof;
16032
+ }
16033
+ }
16013
16034
  return headers;
16014
16035
  }
16015
16036
  url(path) {
@@ -20253,6 +20274,14 @@ var pulseStatusSchema = external_exports2.object({
20253
20274
  resultUrl: external_exports2.string().nullable().optional(),
20254
20275
  reportUrl: external_exports2.string().nullable().optional(),
20255
20276
  retryAfterSeconds: external_exports2.number().int().nullable().optional(),
20277
+ recovery: external_exports2.object({
20278
+ alternateRegionAttempted: external_exports2.literal(true),
20279
+ fallbackScanFrom: external_exports2.string(),
20280
+ noGoReason: external_exports2.string(),
20281
+ primaryScanFrom: external_exports2.string(),
20282
+ primaryScanId: external_exports2.string(),
20283
+ claimedAt: external_exports2.string()
20284
+ }).passthrough().optional(),
20256
20285
  capabilities: pulseCapabilitiesSchema.optional(),
20257
20286
  agentInterpretation: pulseAgentInterpretationSchema.optional(),
20258
20287
  disclaimer: external_exports2.string().optional()
@@ -20295,6 +20324,17 @@ var apiV2PreConsentInventoryKindSchema = external_exports2.enum(["cookie", "trac
20295
20324
  var apiV2PreConsentInventoryPhaseSchema = external_exports2.literal("pre_consent");
20296
20325
  var apiV2PreConsentInventoryPrioritySchema = external_exports2.enum(["high", "medium", "review_needed", "contextual", "unknown"]);
20297
20326
  var apiV2PreConsentInventoryConfidenceSchema = external_exports2.enum(["high", "medium", "low", "unknown"]);
20327
+ var apiV2ScanCreationMetadataShape = {
20328
+ executionMode: external_exports2.enum(["new_scan", "reused_scan"]).optional(),
20329
+ reused: external_exports2.boolean().optional(),
20330
+ reusedScanAgeSeconds: external_exports2.number().int().min(0).nullable().optional(),
20331
+ freshnessDecision: external_exports2.string().optional(),
20332
+ quotaConsumed: external_exports2.boolean().optional(),
20333
+ anonymousQuotaLimit: external_exports2.number().int().min(0).nullable().optional(),
20334
+ anonymousQuotaRemaining: external_exports2.number().int().min(0).nullable().optional(),
20335
+ anonymousQuotaResetAt: external_exports2.string().nullable().optional(),
20336
+ recommendedNextTool: external_exports2.enum(["get_scan_status", "get_scan_bundle"]).optional()
20337
+ };
20298
20338
  var apiV2LinksSchema = external_exports2.object({
20299
20339
  self: external_exports2.string().optional(),
20300
20340
  status: external_exports2.string().optional(),
@@ -20312,7 +20352,8 @@ var apiV2ErrorSchema = external_exports2.object({
20312
20352
  retryAfterSeconds: external_exports2.number().int().nullable().optional()
20313
20353
  }).passthrough(),
20314
20354
  links: apiV2LinksSchema.optional(),
20315
- disclaimer: external_exports2.string().optional()
20355
+ disclaimer: external_exports2.string().optional(),
20356
+ ...apiV2ScanCreationMetadataShape
20316
20357
  }).passthrough();
20317
20358
  var apiV2CreateScanRequestSchema = external_exports2.object({
20318
20359
  url: external_exports2.string().min(1),
@@ -20337,7 +20378,8 @@ var apiV2ScanJobSchema = external_exports2.object({
20337
20378
  lastUpdatedAt: external_exports2.string().optional(),
20338
20379
  retryAfterSeconds: external_exports2.number().int().nullable().optional(),
20339
20380
  links: apiV2LinksSchema.optional(),
20340
- disclaimer: external_exports2.string().optional()
20381
+ disclaimer: external_exports2.string().optional(),
20382
+ ...apiV2ScanCreationMetadataShape
20341
20383
  }).passthrough();
20342
20384
  var apiV2ScanResourceSchema = external_exports2.object({
20343
20385
  type: external_exports2.literal("certscore_scan"),
@@ -20526,6 +20568,13 @@ var mcpCreateScanInputSchema = {
20526
20568
  freshness: mcpPulseFreshnessSchema.optional().describe("Use latest to reuse recent scans or refresh to request a new scan when eligible."),
20527
20569
  scanFrom: mcpScanFromSchema.optional().describe("Optional scan execution context for newly queued scans.")
20528
20570
  };
20571
+ var mcpScanSiteInputSchema = {
20572
+ url: mcpCreateScanInputSchema.url,
20573
+ freshness: mcpCreateScanInputSchema.freshness,
20574
+ scanFrom: mcpCreateScanInputSchema.scanFrom,
20575
+ waitForCompletion: external_exports2.boolean().optional().describe("Wait for a completed scan resource in this tool call. Defaults to true. Set false only for an explicitly asynchronous workflow."),
20576
+ maxWaitSeconds: external_exports2.number().int().min(1).max(45).optional().describe("Maximum time to wait before returning the still-running job. Defaults to 45 seconds; never turns an active scan into an error.")
20577
+ };
20529
20578
  var mcpGetScanStatusInputSchema = {
20530
20579
  jobId: external_exports2.string().min(1).optional().describe("Pulse job ID for a just-created scan that has not yet returned a scanId."),
20531
20580
  scanId: external_exports2.string().min(1).optional().describe("Preferred stable CertScore scan ID for API v2 scan status.")
@@ -20541,6 +20590,11 @@ var mcpGetReportInputSchema = {
20541
20590
  var mcpGetEvidenceInputSchema = {
20542
20591
  scanId: external_exports2.string().min(1).describe("Stable CertScore scan ID.")
20543
20592
  };
20593
+ var mcpGetScanBundleInputSchema = {
20594
+ scanId: external_exports2.string().min(1).describe("Stable CertScore scan ID."),
20595
+ maxFindings: external_exports2.number().int().min(1).max(50).optional().describe("Maximum compact findings to return. Defaults to 20."),
20596
+ maxPreConsentRows: external_exports2.number().int().min(1).max(50).optional().describe("Maximum pre-consent inventory rows to return. Defaults to 20.")
20597
+ };
20544
20598
  var mcpExportFindingsInputSchema = {
20545
20599
  scanId: external_exports2.string().min(1).describe("Stable CertScore scan ID.")
20546
20600
  };
@@ -20634,6 +20688,21 @@ var mcpReportOutputSchema = external_exports2.object({
20634
20688
  noGo: scanNoGoResultSchema.optional(),
20635
20689
  value: external_exports2.string().optional()
20636
20690
  }).passthrough();
20691
+ var mcpScanBundleOutputSchema = external_exports2.object({
20692
+ type: external_exports2.literal("certscore_scan_bundle"),
20693
+ scanId: external_exports2.string(),
20694
+ status: apiV2ScanStatusSchema,
20695
+ resultDisposition: scanResultDispositionSchema.nullable().optional(),
20696
+ noGo: scanNoGoResultSchema.nullable().optional(),
20697
+ scan: apiV2ScanResourceSchema,
20698
+ summary: external_exports2.unknown().nullable(),
20699
+ findings: external_exports2.array(external_exports2.unknown()),
20700
+ evidenceSummary: external_exports2.unknown().nullable(),
20701
+ preConsentCookiesTrackers: external_exports2.unknown().nullable(),
20702
+ links: external_exports2.record(external_exports2.string()).optional(),
20703
+ recommendedNextTool: external_exports2.string().nullable(),
20704
+ disclaimer: external_exports2.string().nullable()
20705
+ }).passthrough();
20637
20706
  var mcpFindingsExportOutputSchema = external_exports2.object({
20638
20707
  type: external_exports2.literal("certscore_mcp_findings_export"),
20639
20708
  scanId: external_exports2.string().nullable(),
@@ -20676,7 +20745,7 @@ var mcpPreConsentCookiesTrackersOutputSchema = apiV2PreConsentCookiesTrackersSch
20676
20745
  var certScoreMcpToolContracts = [
20677
20746
  {
20678
20747
  name: "create_scan",
20679
- title: "Create CertScore Pulse scan",
20748
+ title: "Deprecated \u2014 Create CertScore Pulse scan",
20680
20749
  description: "Deprecated compatibility alias of scan_site. Use scan_site for new integrations. Returns completed-limited no-go disposition and reason-specific guidance when applicable.",
20681
20750
  inputSchema: mcpCreateScanInputSchema,
20682
20751
  outputSchema: mcpCreateScanOutputSchema,
@@ -20685,8 +20754,8 @@ var certScoreMcpToolContracts = [
20685
20754
  {
20686
20755
  name: "scan_site",
20687
20756
  title: "Scan site",
20688
- description: "Start or reuse a CertScore public-web scan. Completed no-go scans return completed_limited status, structured reason-specific guidance, and timing when available.",
20689
- inputSchema: mcpCreateScanInputSchema,
20757
+ description: "Recommended first call. Starts or reuses a public-web scan, reports freshness and anonymous quota decisions, and waits up to 45 seconds by default. If still running, use get_scan_status; otherwise use get_scan_bundle. Completed no-go scans retain completed_limited status and reason-specific guidance.",
20758
+ inputSchema: mcpScanSiteInputSchema,
20690
20759
  outputSchema: mcpScanSiteOutputSchema,
20691
20760
  annotations: scanCreationAnnotations
20692
20761
  },
@@ -20722,6 +20791,14 @@ var certScoreMcpToolContracts = [
20722
20791
  outputSchema: pulseResponseSchema,
20723
20792
  annotations: readOnlyOpenWorldAnnotations
20724
20793
  },
20794
+ {
20795
+ name: "get_scan_bundle",
20796
+ title: "Get compact CertScore scan bundle",
20797
+ description: "Recommended second call after scan_site. Returns the canonical scan state, compact report summary, findings, bounded evidence summary, and pre-consent inventory in one agent-friendly response.",
20798
+ inputSchema: mcpGetScanBundleInputSchema,
20799
+ outputSchema: mcpScanBundleOutputSchema,
20800
+ annotations: readOnlyOpenWorldAnnotations
20801
+ },
20725
20802
  {
20726
20803
  name: "export_findings",
20727
20804
  title: "Export CertScore findings",
@@ -32665,28 +32742,23 @@ function toToolResult(payload) {
32665
32742
  };
32666
32743
  }
32667
32744
  function toToolError(error2) {
32668
- if (error2 instanceof CertScoreError) {
32669
- return {
32670
- ...toToolResult({
32671
- error: {
32672
- name: error2.name,
32673
- message: error2.message,
32674
- status: error2.status,
32675
- code: error2.code,
32676
- retryAfterSeconds: "retryAfterSeconds" in error2 ? error2.retryAfterSeconds : void 0,
32677
- responseBody: truncateErrorResponseBody(error2.responseBody)
32678
- }
32679
- }),
32680
- isError: true
32681
- };
32682
- }
32745
+ const payload = error2 instanceof CertScoreError ? {
32746
+ error: {
32747
+ name: error2.name,
32748
+ message: error2.message,
32749
+ status: error2.status,
32750
+ code: error2.code,
32751
+ retryAfterSeconds: "retryAfterSeconds" in error2 ? error2.retryAfterSeconds : void 0,
32752
+ responseBody: truncateErrorResponseBody(error2.responseBody)
32753
+ }
32754
+ } : {
32755
+ error: {
32756
+ name: error2 instanceof Error ? error2.name : "Error",
32757
+ message: error2 instanceof Error ? error2.message : "Unknown CertScore MCP error."
32758
+ }
32759
+ };
32683
32760
  return {
32684
- ...toToolResult({
32685
- error: {
32686
- name: error2 instanceof Error ? error2.name : "Error",
32687
- message: error2 instanceof Error ? error2.message : "Unknown CertScore MCP error."
32688
- }
32689
- }),
32761
+ content: [{ type: "text", text: JSON.stringify(payload) }],
32690
32762
  isError: true
32691
32763
  };
32692
32764
  }
@@ -32939,6 +33011,71 @@ function limitPreConsentRows(payload, options = {}) {
32939
33011
  }
32940
33012
  };
32941
33013
  }
33014
+ function buildScanBundle(input) {
33015
+ const maxFindings = Math.min(50, Math.max(1, input.maxFindings ?? 20));
33016
+ const maxPreConsentRows = Math.min(50, Math.max(1, input.maxPreConsentRows ?? 20));
33017
+ const evidence = input.evidence;
33018
+ const report = input.report;
33019
+ const findings = Array.isArray(input.findings.findings) ? input.findings.findings.slice(0, maxFindings) : [];
33020
+ const preConsentRows = Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.slice(0, maxPreConsentRows) : [];
33021
+ const links = {
33022
+ ...input.scan.links ?? {},
33023
+ ...report.links && typeof report.links === "object" && !Array.isArray(report.links) ? report.links : {}
33024
+ };
33025
+ return {
33026
+ type: "certscore_scan_bundle",
33027
+ scanId: input.scan.scanId,
33028
+ status: input.scan.status,
33029
+ resultDisposition: input.scan.resultDisposition ?? null,
33030
+ noGo: input.scan.noGo ?? null,
33031
+ scan: input.scan,
33032
+ summary: {
33033
+ summary: report.summary ?? null,
33034
+ executiveSummary: report.executiveSummary ?? null,
33035
+ counts: report.counts ?? null,
33036
+ coverage: report.coverage ?? input.scan.coverage ?? null,
33037
+ agentInterpretation: report.agentInterpretation ?? null
33038
+ },
33039
+ findings,
33040
+ findingsMetadata: {
33041
+ shown: findings.length,
33042
+ total: Array.isArray(input.findings.findings) ? input.findings.findings.length : 0,
33043
+ truncated: Array.isArray(input.findings.findings) && input.findings.findings.length > findings.length
33044
+ },
33045
+ evidenceSummary: {
33046
+ evidenceSafetyNotes: evidence.evidenceSafetyNotes ?? null,
33047
+ projectionDiagnostics: evidence.projectionDiagnostics ?? null,
33048
+ projectedFindings: compactEvidenceValue(evidence.projectedFindings ?? [], {
33049
+ arrayItems: maxFindings,
33050
+ depth: 5,
33051
+ objectKeys: 40,
33052
+ stringChars: 1e3
33053
+ }),
33054
+ coverageDiagnostics: compactEvidenceValue(evidence.coverageDiagnostics ?? null, {
33055
+ arrayItems: 20,
33056
+ depth: 5,
33057
+ objectKeys: 40,
33058
+ stringChars: 1e3
33059
+ }),
33060
+ policySurfaceCoverage: compactEvidenceValue(evidence.policySurfaceCoverage ?? null, {
33061
+ arrayItems: 20,
33062
+ depth: 5,
33063
+ objectKeys: 40,
33064
+ stringChars: 1e3
33065
+ })
33066
+ },
33067
+ preConsentCookiesTrackers: {
33068
+ summary: input.preConsentCookiesTrackers.summary,
33069
+ rows: preConsentRows,
33070
+ shown: preConsentRows.length,
33071
+ total: Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.length : 0,
33072
+ truncated: Array.isArray(input.preConsentCookiesTrackers.rows) && input.preConsentCookiesTrackers.rows.length > preConsentRows.length
33073
+ },
33074
+ links,
33075
+ recommendedNextTool: findings.length > 0 ? "explain_finding" : null,
33076
+ disclaimer: input.scan.disclaimer ?? input.report.disclaimer ?? null
33077
+ };
33078
+ }
32942
33079
  function explainFinding(report, findingId) {
32943
33080
  const finding = findingsFromReport(report).find((candidate) => candidate.id === findingId);
32944
33081
  if (!finding) {
@@ -32976,6 +33113,19 @@ function explainFinding(report, findingId) {
32976
33113
 
32977
33114
  // src/server.ts
32978
33115
  var createScanDeprecationWarningPrinted = false;
33116
+ var DEFAULT_MCP_SCAN_WAIT_MS = 45e3;
33117
+ function scanCreationMetadata(value) {
33118
+ return {
33119
+ executionMode: value.executionMode,
33120
+ reused: value.reused,
33121
+ reusedScanAgeSeconds: value.reusedScanAgeSeconds,
33122
+ freshnessDecision: value.freshnessDecision,
33123
+ quotaConsumed: value.quotaConsumed,
33124
+ anonymousQuotaLimit: value.anonymousQuotaLimit,
33125
+ anonymousQuotaRemaining: value.anonymousQuotaRemaining,
33126
+ anonymousQuotaResetAt: value.anonymousQuotaResetAt
33127
+ };
33128
+ }
32979
33129
  function toolContract(name) {
32980
33130
  const contract = certScoreMcpToolContracts.find((candidate) => candidate.name === name);
32981
33131
  if (!contract) {
@@ -32993,6 +33143,9 @@ function createCertScoreMcpServer(options = {}) {
32993
33143
  const client = new CertScoreClient({
32994
33144
  apiKey: options.apiKey,
32995
33145
  baseUrl: options.baseUrl,
33146
+ clientName: "mcp",
33147
+ forwardedClientIp: options.forwardedClientIp,
33148
+ anonymousRequesterSecret: options.anonymousRequesterSecret,
32996
33149
  timeout: options.timeout
32997
33150
  });
32998
33151
  const server = new McpServer({
@@ -33028,7 +33181,7 @@ function createCertScoreMcpServer(options = {}) {
33028
33181
  try {
33029
33182
  if (!createScanDeprecationWarningPrinted) {
33030
33183
  createScanDeprecationWarningPrinted = true;
33031
- console.error("[certscore-mcp] create_scan is deprecated and will be removed in 0.2.0. Use scan_site.");
33184
+ console.error("[certscore-mcp] create_scan is deprecated in the 0.2.x line. Use scan_site for new integrations.");
33032
33185
  }
33033
33186
  return toToolResult(await createPulseScanTool(input));
33034
33187
  } catch (error2) {
@@ -33041,12 +33194,36 @@ function createCertScoreMcpServer(options = {}) {
33041
33194
  toolContract("scan_site"),
33042
33195
  async (input) => {
33043
33196
  try {
33044
- return toToolResult(
33045
- await client.scans.create(input.url, {
33046
- freshness: input.freshness ?? "latest",
33047
- scanFrom: input.scanFrom
33048
- })
33049
- );
33197
+ const created = await client.scans.create(input.url, {
33198
+ freshness: input.freshness ?? "latest",
33199
+ scanFrom: input.scanFrom
33200
+ });
33201
+ if (input.waitForCompletion === false || created.type === "certscore_scan") {
33202
+ return toToolResult(created);
33203
+ }
33204
+ try {
33205
+ const completed = await client.scans.wait(created, {
33206
+ maxWaitMs: Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1e3 : DEFAULT_MCP_SCAN_WAIT_MS, DEFAULT_MCP_SCAN_WAIT_MS)
33207
+ });
33208
+ return toToolResult({
33209
+ ...completed,
33210
+ ...scanCreationMetadata(created),
33211
+ recommendedNextTool: "get_scan_bundle"
33212
+ });
33213
+ } catch (error2) {
33214
+ if (!(error2 instanceof CertScoreTimeoutError)) {
33215
+ throw error2;
33216
+ }
33217
+ const scanId = created.scanId ?? created.scan_id;
33218
+ if (scanId) {
33219
+ return toToolResult({
33220
+ ...await client.scans.status(scanId),
33221
+ ...scanCreationMetadata(created),
33222
+ recommendedNextTool: "get_scan_status"
33223
+ });
33224
+ }
33225
+ return toToolResult(created);
33226
+ }
33050
33227
  } catch (error2) {
33051
33228
  return toToolError(error2);
33052
33229
  }
@@ -33069,7 +33246,27 @@ function createCertScoreMcpServer(options = {}) {
33069
33246
  async ({ jobId, scanId }) => {
33070
33247
  try {
33071
33248
  if (scanId) {
33072
- return toToolResult(await client.scans.status(scanId));
33249
+ const status2 = await client.scans.status(scanId);
33250
+ const needsTerminalHydration = status2.status === "completed" || status2.status === "completed_limited";
33251
+ if (needsTerminalHydration) {
33252
+ try {
33253
+ const scan = await client.scans.get(scanId);
33254
+ return toToolResult({
33255
+ ...status2,
33256
+ type: "certscore_scan_job",
33257
+ status: scan.status,
33258
+ domain: scan.domain,
33259
+ resultDisposition: scan.resultDisposition,
33260
+ noGo: scan.noGo,
33261
+ startedAt: scan.startedAt,
33262
+ completedAt: scan.completedAt,
33263
+ scanTimeSeconds: scan.scanTimeSeconds,
33264
+ recommendedNextTool: "get_scan_bundle"
33265
+ });
33266
+ } catch {
33267
+ }
33268
+ }
33269
+ return toToolResult(status2);
33073
33270
  }
33074
33271
  if (!jobId) {
33075
33272
  return toToolResult({
@@ -33119,6 +33316,32 @@ function createCertScoreMcpServer(options = {}) {
33119
33316
  }
33120
33317
  }
33121
33318
  );
33319
+ registerTool(
33320
+ "get_scan_bundle",
33321
+ toolContract("get_scan_bundle"),
33322
+ async ({ scanId, maxFindings, maxPreConsentRows }) => {
33323
+ try {
33324
+ const [scan, report, evidence, findings, preConsentCookiesTrackers] = await Promise.all([
33325
+ client.scans.get(scanId),
33326
+ client.getScan(scanId, { detail: "summary", format: "json" }),
33327
+ client.getScan(scanId, { detail: "evidence", format: "json" }),
33328
+ client.findings.list(scanId),
33329
+ client.scans.preConsentCookiesTrackers(scanId)
33330
+ ]);
33331
+ return toToolResult(buildScanBundle({
33332
+ evidence,
33333
+ findings,
33334
+ maxFindings,
33335
+ maxPreConsentRows,
33336
+ preConsentCookiesTrackers,
33337
+ report,
33338
+ scan
33339
+ }));
33340
+ } catch (error2) {
33341
+ return toToolError(error2);
33342
+ }
33343
+ }
33344
+ );
33122
33345
  registerTool(
33123
33346
  "export_findings",
33124
33347
  toolContract("export_findings"),
@@ -33218,12 +33441,13 @@ async function main() {
33218
33441
  "",
33219
33442
  "Usage:",
33220
33443
  " certscore-mcp",
33221
- " certscore-mcp doctor"
33444
+ " certscore-mcp doctor",
33445
+ " certscore-mcp doctor --check-auth"
33222
33446
  ].join("\n"));
33223
33447
  return;
33224
33448
  }
33225
33449
  if (process.argv.includes("doctor")) {
33226
- const result = await getCertScoreMcpDoctorReport();
33450
+ const result = await getCertScoreMcpDoctorReport({ checkAuth: process.argv.includes("--check-auth") });
33227
33451
  console.log(result.lines.join("\n"));
33228
33452
  process.exitCode = result.exitCode;
33229
33453
  return;
@@ -33279,10 +33503,41 @@ async function getCertScoreMcpDoctorReport(options = {}) {
33279
33503
  }
33280
33504
  if (env.CERTSCORE_API_KEY?.trim()) {
33281
33505
  lines.push("[ok] CERTSCORE_API_KEY is present");
33282
- lines.push("[info] No dedicated auth-check endpoint is exposed; verify credentials with a real MCP tool call such as scan_site.");
33506
+ if (options.checkAuth) {
33507
+ if (!healthUrl) {
33508
+ lines.push("[error] Cannot check credentials until CERTSCORE_BASE_URL is valid");
33509
+ exitCode = 1;
33510
+ } else {
33511
+ const authUrl = new URL("/api/v2/auth/check", healthUrl.origin);
33512
+ try {
33513
+ const response = await fetchImpl(authUrl, {
33514
+ headers: {
33515
+ accept: "application/json",
33516
+ authorization: `Bearer ${env.CERTSCORE_API_KEY.trim()}`
33517
+ },
33518
+ signal: AbortSignal.timeout(1e4)
33519
+ });
33520
+ if (response.ok) {
33521
+ lines.push(`[ok] API key authenticated at ${authUrl.href}`);
33522
+ } else {
33523
+ lines.push(`[error] API key rejected with HTTP ${response.status} at ${authUrl.href}`);
33524
+ exitCode = 1;
33525
+ }
33526
+ } catch (error2) {
33527
+ const message = error2 instanceof Error && error2.message ? error2.message : "request failed";
33528
+ lines.push(`[error] API key check failed at ${authUrl.href}: ${message}`);
33529
+ exitCode = 1;
33530
+ }
33531
+ }
33532
+ } else {
33533
+ lines.push("[info] Run certscore-mcp doctor --check-auth to verify the credential without creating a scan.");
33534
+ }
33283
33535
  } else {
33284
33536
  lines.push("[warn] CERTSCORE_API_KEY is not set");
33285
- lines.push("[info] Set CERTSCORE_API_KEY before connecting an MCP client or calling authenticated tools.");
33537
+ lines.push(options.checkAuth ? "[error] --check-auth requires CERTSCORE_API_KEY." : "[info] Set CERTSCORE_API_KEY before connecting an MCP client or calling authenticated tools.");
33538
+ if (options.checkAuth) {
33539
+ exitCode = 1;
33540
+ }
33286
33541
  }
33287
33542
  lines.push("CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.");
33288
33543
  return { exitCode, lines };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { createCertScoreMcpServer } from "./server.js";
2
2
  export { explainFinding, exportFindings, findingsFromReport } from "./tools.js";
3
3
  export { CERTSCORE_MCP_VERSION } from "./version.js";
4
4
  export interface CertScoreMcpDoctorOptions {
5
+ checkAuth?: boolean;
5
6
  env?: Record<string, string | undefined>;
6
7
  fetch?: typeof fetch;
7
8
  nodeVersion?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAMrD,MAAM,WAAW,yBAAyB;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA+CD,wBAAsB,2BAA2B,CAAC,OAAO,GAAE,yBAA8B;;;GA0DxF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAMrD,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgDD,wBAAsB,2BAA2B,CAAC,OAAO,GAAE,yBAA8B;;;GAyFxF"}
package/dist/index.js CHANGED
@@ -34,12 +34,13 @@ async function main() {
34
34
  "",
35
35
  "Usage:",
36
36
  " certscore-mcp",
37
- " certscore-mcp doctor"
37
+ " certscore-mcp doctor",
38
+ " certscore-mcp doctor --check-auth"
38
39
  ].join("\n"));
39
40
  return;
40
41
  }
41
42
  if (process.argv.includes("doctor")) {
42
- const result = await getCertScoreMcpDoctorReport();
43
+ const result = await getCertScoreMcpDoctorReport({ checkAuth: process.argv.includes("--check-auth") });
43
44
  console.log(result.lines.join("\n"));
44
45
  process.exitCode = result.exitCode;
45
46
  return;
@@ -99,11 +100,46 @@ export async function getCertScoreMcpDoctorReport(options = {}) {
99
100
  }
100
101
  if (env.CERTSCORE_API_KEY?.trim()) {
101
102
  lines.push("[ok] CERTSCORE_API_KEY is present");
102
- lines.push("[info] No dedicated auth-check endpoint is exposed; verify credentials with a real MCP tool call such as scan_site.");
103
+ if (options.checkAuth) {
104
+ if (!healthUrl) {
105
+ lines.push("[error] Cannot check credentials until CERTSCORE_BASE_URL is valid");
106
+ exitCode = 1;
107
+ }
108
+ else {
109
+ const authUrl = new URL("/api/v2/auth/check", healthUrl.origin);
110
+ try {
111
+ const response = await fetchImpl(authUrl, {
112
+ headers: {
113
+ accept: "application/json",
114
+ authorization: `Bearer ${env.CERTSCORE_API_KEY.trim()}`
115
+ },
116
+ signal: AbortSignal.timeout(10_000)
117
+ });
118
+ if (response.ok) {
119
+ lines.push(`[ok] API key authenticated at ${authUrl.href}`);
120
+ }
121
+ else {
122
+ lines.push(`[error] API key rejected with HTTP ${response.status} at ${authUrl.href}`);
123
+ exitCode = 1;
124
+ }
125
+ }
126
+ catch (error) {
127
+ const message = error instanceof Error && error.message ? error.message : "request failed";
128
+ lines.push(`[error] API key check failed at ${authUrl.href}: ${message}`);
129
+ exitCode = 1;
130
+ }
131
+ }
132
+ }
133
+ else {
134
+ lines.push("[info] Run certscore-mcp doctor --check-auth to verify the credential without creating a scan.");
135
+ }
103
136
  }
104
137
  else {
105
138
  lines.push("[warn] CERTSCORE_API_KEY is not set");
106
- lines.push("[info] Set CERTSCORE_API_KEY before connecting an MCP client or calling authenticated tools.");
139
+ lines.push(options.checkAuth ? "[error] --check-auth requires CERTSCORE_API_KEY." : "[info] Set CERTSCORE_API_KEY before connecting an MCP client or calling authenticated tools.");
140
+ if (options.checkAuth) {
141
+ exitCode = 1;
142
+ }
107
143
  }
108
144
  lines.push("CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.");
109
145
  return { exitCode, lines };
package/dist/server.d.ts CHANGED
@@ -2,6 +2,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  export interface CertScoreMcpOptions {
3
3
  apiKey?: string;
4
4
  baseUrl?: string;
5
+ forwardedClientIp?: string | null;
6
+ anonymousRequesterSecret?: string | null;
5
7
  timeout?: number;
6
8
  }
7
9
  export declare function createCertScoreMcpServer(options?: CertScoreMcpOptions): McpServer;
@@ -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,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqCD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,mBAAwB,aAwNzE"}
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;CAClB;AAsDD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,mBAAwB,aAoSzE"}
package/dist/server.js CHANGED
@@ -1,9 +1,22 @@
1
- import { CertScoreClient } from "@certscore/sdk";
1
+ import { CertScoreClient, CertScoreTimeoutError } from "@certscore/sdk";
2
2
  import { certScoreMcpToolContracts } 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, exportFindings, limitPreConsentRows, normalizeDetail, normalizeFormat, paginateFindingList, scanIdFromStatus, toToolError, toToolResult } from "./tools.js";
5
+ import { boundEvidencePacket, buildScanBundle, exportFindings, limitPreConsentRows, normalizeDetail, normalizeFormat, paginateFindingList, scanIdFromStatus, toToolError, toToolResult } from "./tools.js";
6
6
  let createScanDeprecationWarningPrinted = false;
7
+ const DEFAULT_MCP_SCAN_WAIT_MS = 45_000;
8
+ function scanCreationMetadata(value) {
9
+ return {
10
+ executionMode: value.executionMode,
11
+ reused: value.reused,
12
+ reusedScanAgeSeconds: value.reusedScanAgeSeconds,
13
+ freshnessDecision: value.freshnessDecision,
14
+ quotaConsumed: value.quotaConsumed,
15
+ anonymousQuotaLimit: value.anonymousQuotaLimit,
16
+ anonymousQuotaRemaining: value.anonymousQuotaRemaining,
17
+ anonymousQuotaResetAt: value.anonymousQuotaResetAt
18
+ };
19
+ }
7
20
  function toolContract(name) {
8
21
  const contract = certScoreMcpToolContracts.find((candidate) => candidate.name === name);
9
22
  if (!contract) {
@@ -21,6 +34,9 @@ export function createCertScoreMcpServer(options = {}) {
21
34
  const client = new CertScoreClient({
22
35
  apiKey: options.apiKey,
23
36
  baseUrl: options.baseUrl,
37
+ clientName: "mcp",
38
+ forwardedClientIp: options.forwardedClientIp,
39
+ anonymousRequesterSecret: options.anonymousRequesterSecret,
24
40
  timeout: options.timeout
25
41
  });
26
42
  const server = new McpServer({
@@ -53,7 +69,7 @@ export function createCertScoreMcpServer(options = {}) {
53
69
  try {
54
70
  if (!createScanDeprecationWarningPrinted) {
55
71
  createScanDeprecationWarningPrinted = true;
56
- console.error("[certscore-mcp] create_scan is deprecated and will be removed in 0.2.0. Use scan_site.");
72
+ console.error("[certscore-mcp] create_scan is deprecated in the 0.2.x line. Use scan_site for new integrations.");
57
73
  }
58
74
  return toToolResult(await createPulseScanTool(input));
59
75
  }
@@ -63,10 +79,37 @@ export function createCertScoreMcpServer(options = {}) {
63
79
  });
64
80
  registerTool("scan_site", toolContract("scan_site"), async (input) => {
65
81
  try {
66
- return toToolResult(await client.scans.create(input.url, {
82
+ const created = await client.scans.create(input.url, {
67
83
  freshness: input.freshness ?? "latest",
68
84
  scanFrom: input.scanFrom
69
- }));
85
+ });
86
+ if (input.waitForCompletion === false || created.type === "certscore_scan") {
87
+ return toToolResult(created);
88
+ }
89
+ try {
90
+ const completed = await client.scans.wait(created, {
91
+ maxWaitMs: Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_WAIT_MS, DEFAULT_MCP_SCAN_WAIT_MS)
92
+ });
93
+ return toToolResult({
94
+ ...completed,
95
+ ...scanCreationMetadata(created),
96
+ recommendedNextTool: "get_scan_bundle"
97
+ });
98
+ }
99
+ catch (error) {
100
+ if (!(error instanceof CertScoreTimeoutError)) {
101
+ throw error;
102
+ }
103
+ const scanId = created.scanId ?? created.scan_id;
104
+ if (scanId) {
105
+ return toToolResult({
106
+ ...(await client.scans.status(scanId)),
107
+ ...scanCreationMetadata(created),
108
+ recommendedNextTool: "get_scan_status"
109
+ });
110
+ }
111
+ return toToolResult(created);
112
+ }
70
113
  }
71
114
  catch (error) {
72
115
  return toToolError(error);
@@ -83,7 +126,30 @@ export function createCertScoreMcpServer(options = {}) {
83
126
  registerTool("get_scan_status", toolContract("get_scan_status"), async ({ jobId, scanId }) => {
84
127
  try {
85
128
  if (scanId) {
86
- return toToolResult(await client.scans.status(scanId));
129
+ const status = await client.scans.status(scanId);
130
+ const needsTerminalHydration = status.status === "completed" || status.status === "completed_limited";
131
+ if (needsTerminalHydration) {
132
+ try {
133
+ const scan = await client.scans.get(scanId);
134
+ return toToolResult({
135
+ ...status,
136
+ type: "certscore_scan_job",
137
+ status: scan.status,
138
+ domain: scan.domain,
139
+ resultDisposition: scan.resultDisposition,
140
+ noGo: scan.noGo,
141
+ startedAt: scan.startedAt,
142
+ completedAt: scan.completedAt,
143
+ scanTimeSeconds: scan.scanTimeSeconds,
144
+ recommendedNextTool: "get_scan_bundle"
145
+ });
146
+ }
147
+ catch {
148
+ // Preserve the API status response if the terminal scan resource is
149
+ // briefly unavailable during eventual-consistency windows.
150
+ }
151
+ }
152
+ return toToolResult(status);
87
153
  }
88
154
  if (!jobId) {
89
155
  return toToolResult({
@@ -129,6 +195,29 @@ export function createCertScoreMcpServer(options = {}) {
129
195
  return toToolError(error);
130
196
  }
131
197
  });
198
+ registerTool("get_scan_bundle", toolContract("get_scan_bundle"), async ({ scanId, maxFindings, maxPreConsentRows }) => {
199
+ try {
200
+ const [scan, report, evidence, findings, preConsentCookiesTrackers] = await Promise.all([
201
+ client.scans.get(scanId),
202
+ client.getScan(scanId, { detail: "summary", format: "json" }),
203
+ client.getScan(scanId, { detail: "evidence", format: "json" }),
204
+ client.findings.list(scanId),
205
+ client.scans.preConsentCookiesTrackers(scanId)
206
+ ]);
207
+ return toToolResult(buildScanBundle({
208
+ evidence,
209
+ findings,
210
+ maxFindings,
211
+ maxPreConsentRows,
212
+ preConsentCookiesTrackers,
213
+ report,
214
+ scan
215
+ }));
216
+ }
217
+ catch (error) {
218
+ return toToolError(error);
219
+ }
220
+ });
132
221
  registerTool("export_findings", toolContract("export_findings"), async ({ scanId }) => {
133
222
  try {
134
223
  const report = await client.getScan(scanId, { detail: "full", format: "json" });
package/dist/tools.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
- import { type JobStatus, type PulseDetail, type PulseFormat, type PulseResult, type TopFinding } from "@certscore/sdk";
2
+ import { type FindingList, type JobStatus, type PreConsentCookiesTrackers, type PulseDetail, type PulseFormat, type PulseResult, type ScanResource, type TopFinding } from "@certscore/sdk";
3
3
  export declare const MAX_EVIDENCE_PACKET_CHARS = 250000;
4
4
  export declare function toToolResult(payload: unknown): CallToolResult;
5
5
  export declare function toToolError(error: unknown): CallToolResult;
@@ -37,6 +37,69 @@ export declare function paginateFindingList<T extends Record<string, unknown>>(p
37
37
  export declare function limitPreConsentRows<T extends Record<string, unknown>>(payload: T, options?: {
38
38
  maxRows?: number;
39
39
  }): T;
40
+ export declare function buildScanBundle(input: {
41
+ evidence: PulseResult;
42
+ findings: FindingList;
43
+ maxFindings?: number;
44
+ maxPreConsentRows?: number;
45
+ preConsentCookiesTrackers: PreConsentCookiesTrackers;
46
+ report: PulseResult;
47
+ scan: ScanResource;
48
+ }): {
49
+ type: string;
50
+ scanId: string;
51
+ status: import("packages/certscore-sdk/dist/types.js").PulseJobStatus;
52
+ resultDisposition: "no_go" | null;
53
+ noGo: import("@certscore/sdk").ScanNoGoResult | null;
54
+ scan: ScanResource;
55
+ summary: {
56
+ summary: {} | null;
57
+ executiveSummary: {} | null;
58
+ counts: {} | null;
59
+ coverage: {} | null;
60
+ agentInterpretation: {} | null;
61
+ };
62
+ findings: import("packages/certscore-sdk/dist/types.js").FindingSummary[];
63
+ findingsMetadata: {
64
+ shown: number;
65
+ total: number;
66
+ truncated: boolean;
67
+ };
68
+ evidenceSummary: {
69
+ evidenceSafetyNotes: {} | null;
70
+ projectionDiagnostics: {} | null;
71
+ projectedFindings: unknown;
72
+ coverageDiagnostics: unknown;
73
+ policySurfaceCoverage: unknown;
74
+ };
75
+ preConsentCookiesTrackers: {
76
+ summary: {
77
+ [key: string]: unknown;
78
+ rowCount: number;
79
+ trackerCount: number;
80
+ cookieCount: number;
81
+ requestCount: number;
82
+ totalRowCount?: number;
83
+ truncated?: boolean;
84
+ };
85
+ rows: import("packages/certscore-sdk/dist/types.js").PreConsentCookiesTrackersRow[];
86
+ shown: number;
87
+ total: number;
88
+ truncated: boolean;
89
+ };
90
+ links: {
91
+ [key: string]: string | undefined;
92
+ self?: string;
93
+ status?: string;
94
+ findings?: string;
95
+ pulse?: string;
96
+ report?: string;
97
+ latestDomainScan?: string;
98
+ docs?: string;
99
+ };
100
+ recommendedNextTool: string | null;
101
+ disclaimer: string | null;
102
+ };
40
103
  export declare function explainFinding(report: PulseResult, findingId: string): {
41
104
  type: string;
42
105
  scanId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAkB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGvI,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAKjD,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,CAa7D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAyB1D;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,kBAAkB,SAA4B,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0C9H;AA6ID,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,CAE5E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,CAE5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,iBAEjD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,iBAGlD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,EAAE,CAQpE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;EAsBjD;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,OAAO,EAAE,CAAC,EACV,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,CAAC,CAoBH;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,OAAO,EAAE,CAAC,EACV,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,CAAC,CAiBH;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCpE"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,yBAAyB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG5M,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAKjD,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,CAa7D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CA0B1D;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,kBAAkB,SAA4B,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0C9H;AA6ID,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,CAE5E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,CAE5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,iBAEjD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,iBAGlD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,EAAE,CAQpE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;EAsBjD;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,OAAO,EAAE,CAAC,EACV,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,CAAC,CAoBH;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,OAAO,EAAE,CAAC,EACV,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,CAAC,CAiBH;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,yBAAyB,CAAC;IACrD,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;CACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmEA;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCpE"}
package/dist/tools.js CHANGED
@@ -19,28 +19,28 @@ export function toToolResult(payload) {
19
19
  };
20
20
  }
21
21
  export function toToolError(error) {
22
- if (error instanceof CertScoreError) {
23
- return {
24
- ...toToolResult({
25
- error: {
26
- name: error.name,
27
- message: error.message,
28
- status: error.status,
29
- code: error.code,
30
- retryAfterSeconds: "retryAfterSeconds" in error ? error.retryAfterSeconds : undefined,
31
- responseBody: truncateErrorResponseBody(error.responseBody)
32
- }
33
- }),
34
- isError: true
35
- };
36
- }
37
- return {
38
- ...toToolResult({
22
+ const payload = error instanceof CertScoreError
23
+ ? {
24
+ error: {
25
+ name: error.name,
26
+ message: error.message,
27
+ status: error.status,
28
+ code: error.code,
29
+ retryAfterSeconds: "retryAfterSeconds" in error ? error.retryAfterSeconds : undefined,
30
+ responseBody: truncateErrorResponseBody(error.responseBody)
31
+ }
32
+ }
33
+ : {
39
34
  error: {
40
35
  name: error instanceof Error ? error.name : "Error",
41
36
  message: error instanceof Error ? error.message : "Unknown CertScore MCP error."
42
37
  }
43
- }),
38
+ };
39
+ // MCP validates structuredContent against the tool's success output schema,
40
+ // including for isError results. Keep errors in text content so a bounded
41
+ // error envelope cannot be rejected as an invalid success resource.
42
+ return {
43
+ content: [{ type: "text", text: JSON.stringify(payload) }],
44
44
  isError: true
45
45
  };
46
46
  }
@@ -297,6 +297,73 @@ export function limitPreConsentRows(payload, options = {}) {
297
297
  }
298
298
  };
299
299
  }
300
+ export function buildScanBundle(input) {
301
+ const maxFindings = Math.min(50, Math.max(1, input.maxFindings ?? 20));
302
+ const maxPreConsentRows = Math.min(50, Math.max(1, input.maxPreConsentRows ?? 20));
303
+ const evidence = input.evidence;
304
+ const report = input.report;
305
+ const findings = Array.isArray(input.findings.findings) ? input.findings.findings.slice(0, maxFindings) : [];
306
+ const preConsentRows = Array.isArray(input.preConsentCookiesTrackers.rows)
307
+ ? input.preConsentCookiesTrackers.rows.slice(0, maxPreConsentRows)
308
+ : [];
309
+ const links = {
310
+ ...(input.scan.links ?? {}),
311
+ ...(report.links && typeof report.links === "object" && !Array.isArray(report.links) ? report.links : {})
312
+ };
313
+ return {
314
+ type: "certscore_scan_bundle",
315
+ scanId: input.scan.scanId,
316
+ status: input.scan.status,
317
+ resultDisposition: input.scan.resultDisposition ?? null,
318
+ noGo: input.scan.noGo ?? null,
319
+ scan: input.scan,
320
+ summary: {
321
+ summary: report.summary ?? null,
322
+ executiveSummary: report.executiveSummary ?? null,
323
+ counts: report.counts ?? null,
324
+ coverage: report.coverage ?? input.scan.coverage ?? null,
325
+ agentInterpretation: report.agentInterpretation ?? null
326
+ },
327
+ findings,
328
+ findingsMetadata: {
329
+ shown: findings.length,
330
+ total: Array.isArray(input.findings.findings) ? input.findings.findings.length : 0,
331
+ truncated: Array.isArray(input.findings.findings) && input.findings.findings.length > findings.length
332
+ },
333
+ evidenceSummary: {
334
+ evidenceSafetyNotes: evidence.evidenceSafetyNotes ?? null,
335
+ projectionDiagnostics: evidence.projectionDiagnostics ?? null,
336
+ projectedFindings: compactEvidenceValue(evidence.projectedFindings ?? [], {
337
+ arrayItems: maxFindings,
338
+ depth: 5,
339
+ objectKeys: 40,
340
+ stringChars: 1_000
341
+ }),
342
+ coverageDiagnostics: compactEvidenceValue(evidence.coverageDiagnostics ?? null, {
343
+ arrayItems: 20,
344
+ depth: 5,
345
+ objectKeys: 40,
346
+ stringChars: 1_000
347
+ }),
348
+ policySurfaceCoverage: compactEvidenceValue(evidence.policySurfaceCoverage ?? null, {
349
+ arrayItems: 20,
350
+ depth: 5,
351
+ objectKeys: 40,
352
+ stringChars: 1_000
353
+ })
354
+ },
355
+ preConsentCookiesTrackers: {
356
+ summary: input.preConsentCookiesTrackers.summary,
357
+ rows: preConsentRows,
358
+ shown: preConsentRows.length,
359
+ total: Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.length : 0,
360
+ truncated: Array.isArray(input.preConsentCookiesTrackers.rows) && input.preConsentCookiesTrackers.rows.length > preConsentRows.length
361
+ },
362
+ links,
363
+ recommendedNextTool: findings.length > 0 ? "explain_finding" : null,
364
+ disclaimer: input.scan.disclaimer ?? input.report.disclaimer ?? null
365
+ };
366
+ }
300
367
  export function explainFinding(report, findingId) {
301
368
  const finding = findingsFromReport(report).find((candidate) => candidate.id === findingId);
302
369
  if (!finding) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certscore/mcp",
3
- "version": "0.2.6",
3
+ "version": "0.2.11",
4
4
  "mcpName": "ai.certscore/mcp",
5
5
  "private": false,
6
6
  "description": "MCP server for CertScore public website risk-signal workflows.",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "url": "https://github.com/ergoveritas1-alt/certscore.ai",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.2.6",
10
+ "version": "0.2.11",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@certscore/mcp",
16
- "version": "0.2.6",
16
+ "version": "0.2.11",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },