@certscore/mcp 0.2.7 → 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.7 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",
@@ -32934,6 +33011,71 @@ function limitPreConsentRows(payload, options = {}) {
32934
33011
  }
32935
33012
  };
32936
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
+ }
32937
33079
  function explainFinding(report, findingId) {
32938
33080
  const finding = findingsFromReport(report).find((candidate) => candidate.id === findingId);
32939
33081
  if (!finding) {
@@ -32971,6 +33113,19 @@ function explainFinding(report, findingId) {
32971
33113
 
32972
33114
  // src/server.ts
32973
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
+ }
32974
33129
  function toolContract(name) {
32975
33130
  const contract = certScoreMcpToolContracts.find((candidate) => candidate.name === name);
32976
33131
  if (!contract) {
@@ -32988,6 +33143,9 @@ function createCertScoreMcpServer(options = {}) {
32988
33143
  const client = new CertScoreClient({
32989
33144
  apiKey: options.apiKey,
32990
33145
  baseUrl: options.baseUrl,
33146
+ clientName: "mcp",
33147
+ forwardedClientIp: options.forwardedClientIp,
33148
+ anonymousRequesterSecret: options.anonymousRequesterSecret,
32991
33149
  timeout: options.timeout
32992
33150
  });
32993
33151
  const server = new McpServer({
@@ -33023,7 +33181,7 @@ function createCertScoreMcpServer(options = {}) {
33023
33181
  try {
33024
33182
  if (!createScanDeprecationWarningPrinted) {
33025
33183
  createScanDeprecationWarningPrinted = true;
33026
- 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.");
33027
33185
  }
33028
33186
  return toToolResult(await createPulseScanTool(input));
33029
33187
  } catch (error2) {
@@ -33036,12 +33194,36 @@ function createCertScoreMcpServer(options = {}) {
33036
33194
  toolContract("scan_site"),
33037
33195
  async (input) => {
33038
33196
  try {
33039
- return toToolResult(
33040
- await client.scans.create(input.url, {
33041
- freshness: input.freshness ?? "latest",
33042
- scanFrom: input.scanFrom
33043
- })
33044
- );
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
+ }
33045
33227
  } catch (error2) {
33046
33228
  return toToolError(error2);
33047
33229
  }
@@ -33064,7 +33246,27 @@ function createCertScoreMcpServer(options = {}) {
33064
33246
  async ({ jobId, scanId }) => {
33065
33247
  try {
33066
33248
  if (scanId) {
33067
- 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);
33068
33270
  }
33069
33271
  if (!jobId) {
33070
33272
  return toToolResult({
@@ -33114,6 +33316,32 @@ function createCertScoreMcpServer(options = {}) {
33114
33316
  }
33115
33317
  }
33116
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
+ );
33117
33345
  registerTool(
33118
33346
  "export_findings",
33119
33347
  toolContract("export_findings"),
@@ -33213,12 +33441,13 @@ async function main() {
33213
33441
  "",
33214
33442
  "Usage:",
33215
33443
  " certscore-mcp",
33216
- " certscore-mcp doctor"
33444
+ " certscore-mcp doctor",
33445
+ " certscore-mcp doctor --check-auth"
33217
33446
  ].join("\n"));
33218
33447
  return;
33219
33448
  }
33220
33449
  if (process.argv.includes("doctor")) {
33221
- const result = await getCertScoreMcpDoctorReport();
33450
+ const result = await getCertScoreMcpDoctorReport({ checkAuth: process.argv.includes("--check-auth") });
33222
33451
  console.log(result.lines.join("\n"));
33223
33452
  process.exitCode = result.exitCode;
33224
33453
  return;
@@ -33274,10 +33503,41 @@ async function getCertScoreMcpDoctorReport(options = {}) {
33274
33503
  }
33275
33504
  if (env.CERTSCORE_API_KEY?.trim()) {
33276
33505
  lines.push("[ok] CERTSCORE_API_KEY is present");
33277
- 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
+ }
33278
33535
  } else {
33279
33536
  lines.push("[warn] CERTSCORE_API_KEY is not set");
33280
- 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
+ }
33281
33541
  }
33282
33542
  lines.push("CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.");
33283
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,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,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
@@ -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.7",
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.",
@@ -46,6 +46,14 @@
46
46
  "url": "https://certscore.ai/contact",
47
47
  "email": "support@certscore.ai"
48
48
  },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --banner:js='#!/usr/bin/env node' --outfile=dist/certscore-mcp.mjs",
51
+ "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --banner:js='#!/usr/bin/env node' --outfile=dist/certscore-mcp.mjs",
52
+ "typecheck": "tsc --noEmit -p tsconfig.json",
53
+ "test": "node --import tsx --test src/*.test.ts",
54
+ "prepack": "pnpm run build",
55
+ "clean": "rm -rf dist"
56
+ },
49
57
  "keywords": [
50
58
  "certscore",
51
59
  "pulse",
@@ -59,14 +67,7 @@
59
67
  "zod": "^3.25.76"
60
68
  },
61
69
  "devDependencies": {
62
- "@certscore/api-contracts": "0.1.0",
63
- "@certscore/sdk": "0.2.4"
64
- },
65
- "scripts": {
66
- "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --banner:js='#!/usr/bin/env node' --outfile=dist/certscore-mcp.mjs",
67
- "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --banner:js='#!/usr/bin/env node' --outfile=dist/certscore-mcp.mjs",
68
- "typecheck": "tsc --noEmit -p tsconfig.json",
69
- "test": "node --import tsx --test src/*.test.ts",
70
- "clean": "rm -rf dist"
70
+ "@certscore/api-contracts": "workspace:*",
71
+ "@certscore/sdk": "workspace:*"
71
72
  }
72
- }
73
+ }
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.7",
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.7",
16
+ "version": "0.2.11",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },