@certscore/mcp 0.2.7 → 0.2.12

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.12 adds the focused hosted CertScore Light workflow and current remote registry metadata. 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,22 @@ 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
+ For the simplest no-account workflow, use CertScore Light:
70
+
71
+ ```text
72
+ https://mcp.certscore.ai/mcp/light
73
+ ```
74
+
75
+ Light exposes only `scan_site`, `get_scan_status`, and `get_scan_bundle`. The allowance is 20 new scans per requester IP per UTC day.
76
+ Eligible recent-result reuse does not consume the allowance. Every no-account response includes the higher-volume contact path at
77
+ `support@certscore.ai`.
78
+
62
79
  ## Configuration
63
80
 
64
81
  Install with Homebrew on macOS:
@@ -124,9 +141,22 @@ Stdio API keys use `pulse:read` and `mcp`; creating scans additionally requires
124
141
  certscore-mcp --version
125
142
  certscore-mcp --help
126
143
  CERTSCORE_API_KEY=... certscore-mcp doctor
144
+ CERTSCORE_API_KEY=... certscore-mcp doctor --check-auth
127
145
  ```
128
146
 
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.
147
+ 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.
148
+
149
+ ## No-account agent scan path
150
+
151
+ Agents that cannot create an account or configure OAuth can use the public API v2 scan path without an `Authorization` header:
152
+
153
+ ```bash
154
+ curl -X POST https://certscore.ai/api/v2/scans \
155
+ -H "Content-Type: application/json" \
156
+ -d '{"url":"https://example.com","freshness":"latest","scanFrom":"eu_ie"}'
157
+ ```
158
+
159
+ New anonymous scans are limited to 20 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. Contact `support@certscore.ai` for a higher-volume allowance, including when reuse serves the current request.
130
160
 
131
161
  ## MCP Client Examples
132
162
 
@@ -198,14 +228,16 @@ Local repo config for contributors:
198
228
 
199
229
  ## Agent Workflow
200
230
 
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.
231
+ 1. Call `scan_site` with a public URL. It normally returns the completed scan resource in the same tool call.
232
+ 2. Only if it returns a non-terminal job, call `get_scan_status` using the stable `scanId` until completion.
233
+ 3. Call `get_scan_bundle` for the normal compact review handoff.
234
+ 4. Call `get_report`, `get_evidence`, `list_findings`, or `get_pre_consent_cookies_trackers` only when the task needs a dedicated view.
235
+ 5. Call `explain_finding` when a reviewer needs evidence and caveats for a specific finding.
236
+ 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.
237
+
238
+ `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.
239
+
240
+ 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
241
 
210
242
  ```json
211
243
  {
@@ -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,19 @@ 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
+ upgradeSupportEmail: external_exports2.string().email().nullable().optional(),
20337
+ upgradeMessage: external_exports2.string().nullable().optional(),
20338
+ recommendedNextTool: external_exports2.enum(["get_scan_status", "get_scan_bundle"]).optional()
20339
+ };
20298
20340
  var apiV2LinksSchema = external_exports2.object({
20299
20341
  self: external_exports2.string().optional(),
20300
20342
  status: external_exports2.string().optional(),
@@ -20312,7 +20354,8 @@ var apiV2ErrorSchema = external_exports2.object({
20312
20354
  retryAfterSeconds: external_exports2.number().int().nullable().optional()
20313
20355
  }).passthrough(),
20314
20356
  links: apiV2LinksSchema.optional(),
20315
- disclaimer: external_exports2.string().optional()
20357
+ disclaimer: external_exports2.string().optional(),
20358
+ ...apiV2ScanCreationMetadataShape
20316
20359
  }).passthrough();
20317
20360
  var apiV2CreateScanRequestSchema = external_exports2.object({
20318
20361
  url: external_exports2.string().min(1),
@@ -20337,7 +20380,8 @@ var apiV2ScanJobSchema = external_exports2.object({
20337
20380
  lastUpdatedAt: external_exports2.string().optional(),
20338
20381
  retryAfterSeconds: external_exports2.number().int().nullable().optional(),
20339
20382
  links: apiV2LinksSchema.optional(),
20340
- disclaimer: external_exports2.string().optional()
20383
+ disclaimer: external_exports2.string().optional(),
20384
+ ...apiV2ScanCreationMetadataShape
20341
20385
  }).passthrough();
20342
20386
  var apiV2ScanResourceSchema = external_exports2.object({
20343
20387
  type: external_exports2.literal("certscore_scan"),
@@ -20526,6 +20570,13 @@ var mcpCreateScanInputSchema = {
20526
20570
  freshness: mcpPulseFreshnessSchema.optional().describe("Use latest to reuse recent scans or refresh to request a new scan when eligible."),
20527
20571
  scanFrom: mcpScanFromSchema.optional().describe("Optional scan execution context for newly queued scans.")
20528
20572
  };
20573
+ var mcpScanSiteInputSchema = {
20574
+ url: mcpCreateScanInputSchema.url,
20575
+ freshness: mcpCreateScanInputSchema.freshness,
20576
+ scanFrom: mcpCreateScanInputSchema.scanFrom,
20577
+ 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."),
20578
+ 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.")
20579
+ };
20529
20580
  var mcpGetScanStatusInputSchema = {
20530
20581
  jobId: external_exports2.string().min(1).optional().describe("Pulse job ID for a just-created scan that has not yet returned a scanId."),
20531
20582
  scanId: external_exports2.string().min(1).optional().describe("Preferred stable CertScore scan ID for API v2 scan status.")
@@ -20541,6 +20592,11 @@ var mcpGetReportInputSchema = {
20541
20592
  var mcpGetEvidenceInputSchema = {
20542
20593
  scanId: external_exports2.string().min(1).describe("Stable CertScore scan ID.")
20543
20594
  };
20595
+ var mcpGetScanBundleInputSchema = {
20596
+ scanId: external_exports2.string().min(1).describe("Stable CertScore scan ID."),
20597
+ maxFindings: external_exports2.number().int().min(1).max(50).optional().describe("Maximum compact findings to return. Defaults to 20."),
20598
+ maxPreConsentRows: external_exports2.number().int().min(1).max(50).optional().describe("Maximum pre-consent inventory rows to return. Defaults to 20.")
20599
+ };
20544
20600
  var mcpExportFindingsInputSchema = {
20545
20601
  scanId: external_exports2.string().min(1).describe("Stable CertScore scan ID.")
20546
20602
  };
@@ -20634,6 +20690,21 @@ var mcpReportOutputSchema = external_exports2.object({
20634
20690
  noGo: scanNoGoResultSchema.optional(),
20635
20691
  value: external_exports2.string().optional()
20636
20692
  }).passthrough();
20693
+ var mcpScanBundleOutputSchema = external_exports2.object({
20694
+ type: external_exports2.literal("certscore_scan_bundle"),
20695
+ scanId: external_exports2.string(),
20696
+ status: apiV2ScanStatusSchema,
20697
+ resultDisposition: scanResultDispositionSchema.nullable().optional(),
20698
+ noGo: scanNoGoResultSchema.nullable().optional(),
20699
+ scan: apiV2ScanResourceSchema,
20700
+ summary: external_exports2.unknown().nullable(),
20701
+ findings: external_exports2.array(external_exports2.unknown()),
20702
+ evidenceSummary: external_exports2.unknown().nullable(),
20703
+ preConsentCookiesTrackers: external_exports2.unknown().nullable(),
20704
+ links: external_exports2.record(external_exports2.string()).optional(),
20705
+ recommendedNextTool: external_exports2.string().nullable(),
20706
+ disclaimer: external_exports2.string().nullable()
20707
+ }).passthrough();
20637
20708
  var mcpFindingsExportOutputSchema = external_exports2.object({
20638
20709
  type: external_exports2.literal("certscore_mcp_findings_export"),
20639
20710
  scanId: external_exports2.string().nullable(),
@@ -20676,7 +20747,7 @@ var mcpPreConsentCookiesTrackersOutputSchema = apiV2PreConsentCookiesTrackersSch
20676
20747
  var certScoreMcpToolContracts = [
20677
20748
  {
20678
20749
  name: "create_scan",
20679
- title: "Create CertScore Pulse scan",
20750
+ title: "Deprecated \u2014 Create CertScore Pulse scan",
20680
20751
  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
20752
  inputSchema: mcpCreateScanInputSchema,
20682
20753
  outputSchema: mcpCreateScanOutputSchema,
@@ -20685,8 +20756,8 @@ var certScoreMcpToolContracts = [
20685
20756
  {
20686
20757
  name: "scan_site",
20687
20758
  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,
20759
+ 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.",
20760
+ inputSchema: mcpScanSiteInputSchema,
20690
20761
  outputSchema: mcpScanSiteOutputSchema,
20691
20762
  annotations: scanCreationAnnotations
20692
20763
  },
@@ -20722,6 +20793,14 @@ var certScoreMcpToolContracts = [
20722
20793
  outputSchema: pulseResponseSchema,
20723
20794
  annotations: readOnlyOpenWorldAnnotations
20724
20795
  },
20796
+ {
20797
+ name: "get_scan_bundle",
20798
+ title: "Get compact CertScore scan bundle",
20799
+ 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.",
20800
+ inputSchema: mcpGetScanBundleInputSchema,
20801
+ outputSchema: mcpScanBundleOutputSchema,
20802
+ annotations: readOnlyOpenWorldAnnotations
20803
+ },
20725
20804
  {
20726
20805
  name: "export_findings",
20727
20806
  title: "Export CertScore findings",
@@ -32934,6 +33013,71 @@ function limitPreConsentRows(payload, options = {}) {
32934
33013
  }
32935
33014
  };
32936
33015
  }
33016
+ function buildScanBundle(input) {
33017
+ const maxFindings = Math.min(50, Math.max(1, input.maxFindings ?? 20));
33018
+ const maxPreConsentRows = Math.min(50, Math.max(1, input.maxPreConsentRows ?? 20));
33019
+ const evidence = input.evidence;
33020
+ const report = input.report;
33021
+ const findings = Array.isArray(input.findings.findings) ? input.findings.findings.slice(0, maxFindings) : [];
33022
+ const preConsentRows = Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.slice(0, maxPreConsentRows) : [];
33023
+ const links = {
33024
+ ...input.scan.links ?? {},
33025
+ ...report.links && typeof report.links === "object" && !Array.isArray(report.links) ? report.links : {}
33026
+ };
33027
+ return {
33028
+ type: "certscore_scan_bundle",
33029
+ scanId: input.scan.scanId,
33030
+ status: input.scan.status,
33031
+ resultDisposition: input.scan.resultDisposition ?? null,
33032
+ noGo: input.scan.noGo ?? null,
33033
+ scan: input.scan,
33034
+ summary: {
33035
+ summary: report.summary ?? null,
33036
+ executiveSummary: report.executiveSummary ?? null,
33037
+ counts: report.counts ?? null,
33038
+ coverage: report.coverage ?? input.scan.coverage ?? null,
33039
+ agentInterpretation: report.agentInterpretation ?? null
33040
+ },
33041
+ findings,
33042
+ findingsMetadata: {
33043
+ shown: findings.length,
33044
+ total: Array.isArray(input.findings.findings) ? input.findings.findings.length : 0,
33045
+ truncated: Array.isArray(input.findings.findings) && input.findings.findings.length > findings.length
33046
+ },
33047
+ evidenceSummary: {
33048
+ evidenceSafetyNotes: evidence.evidenceSafetyNotes ?? null,
33049
+ projectionDiagnostics: evidence.projectionDiagnostics ?? null,
33050
+ projectedFindings: compactEvidenceValue(evidence.projectedFindings ?? [], {
33051
+ arrayItems: maxFindings,
33052
+ depth: 5,
33053
+ objectKeys: 40,
33054
+ stringChars: 1e3
33055
+ }),
33056
+ coverageDiagnostics: compactEvidenceValue(evidence.coverageDiagnostics ?? null, {
33057
+ arrayItems: 20,
33058
+ depth: 5,
33059
+ objectKeys: 40,
33060
+ stringChars: 1e3
33061
+ }),
33062
+ policySurfaceCoverage: compactEvidenceValue(evidence.policySurfaceCoverage ?? null, {
33063
+ arrayItems: 20,
33064
+ depth: 5,
33065
+ objectKeys: 40,
33066
+ stringChars: 1e3
33067
+ })
33068
+ },
33069
+ preConsentCookiesTrackers: {
33070
+ summary: input.preConsentCookiesTrackers.summary,
33071
+ rows: preConsentRows,
33072
+ shown: preConsentRows.length,
33073
+ total: Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.length : 0,
33074
+ truncated: Array.isArray(input.preConsentCookiesTrackers.rows) && input.preConsentCookiesTrackers.rows.length > preConsentRows.length
33075
+ },
33076
+ links,
33077
+ recommendedNextTool: findings.length > 0 ? "explain_finding" : null,
33078
+ disclaimer: input.scan.disclaimer ?? input.report.disclaimer ?? null
33079
+ };
33080
+ }
32937
33081
  function explainFinding(report, findingId) {
32938
33082
  const finding = findingsFromReport(report).find((candidate) => candidate.id === findingId);
32939
33083
  if (!finding) {
@@ -32971,6 +33115,21 @@ function explainFinding(report, findingId) {
32971
33115
 
32972
33116
  // src/server.ts
32973
33117
  var createScanDeprecationWarningPrinted = false;
33118
+ var DEFAULT_MCP_SCAN_WAIT_MS = 45e3;
33119
+ function scanCreationMetadata(value) {
33120
+ return {
33121
+ executionMode: value.executionMode,
33122
+ reused: value.reused,
33123
+ reusedScanAgeSeconds: value.reusedScanAgeSeconds,
33124
+ freshnessDecision: value.freshnessDecision,
33125
+ quotaConsumed: value.quotaConsumed,
33126
+ anonymousQuotaLimit: value.anonymousQuotaLimit,
33127
+ anonymousQuotaRemaining: value.anonymousQuotaRemaining,
33128
+ anonymousQuotaResetAt: value.anonymousQuotaResetAt,
33129
+ upgradeSupportEmail: value.upgradeSupportEmail,
33130
+ upgradeMessage: value.upgradeMessage
33131
+ };
33132
+ }
32974
33133
  function toolContract(name) {
32975
33134
  const contract = certScoreMcpToolContracts.find((candidate) => candidate.name === name);
32976
33135
  if (!contract) {
@@ -32988,13 +33147,23 @@ function createCertScoreMcpServer(options = {}) {
32988
33147
  const client = new CertScoreClient({
32989
33148
  apiKey: options.apiKey,
32990
33149
  baseUrl: options.baseUrl,
33150
+ clientName: "mcp",
33151
+ forwardedClientIp: options.forwardedClientIp,
33152
+ anonymousRequesterSecret: options.anonymousRequesterSecret,
32991
33153
  timeout: options.timeout
32992
33154
  });
32993
33155
  const server = new McpServer({
32994
33156
  name: "certscore",
32995
33157
  version: CERTSCORE_MCP_VERSION
32996
33158
  });
32997
- const registerTool = server.registerTool.bind(server);
33159
+ const lightTools = /* @__PURE__ */ new Set(["scan_site", "get_scan_status", "get_scan_bundle"]);
33160
+ const registerMcpTool = server.registerTool.bind(server);
33161
+ const registerTool = (name, contract, handler) => {
33162
+ if (options.toolProfile === "light" && !lightTools.has(name)) {
33163
+ return;
33164
+ }
33165
+ registerMcpTool(name, contract, handler);
33166
+ };
32998
33167
  async function createPulseScanTool(input) {
32999
33168
  const result = await client.submitScan(input.url, {
33000
33169
  detail: normalizeDetail2(input.detail),
@@ -33023,7 +33192,7 @@ function createCertScoreMcpServer(options = {}) {
33023
33192
  try {
33024
33193
  if (!createScanDeprecationWarningPrinted) {
33025
33194
  createScanDeprecationWarningPrinted = true;
33026
- console.error("[certscore-mcp] create_scan is deprecated and will be removed in 0.2.0. Use scan_site.");
33195
+ console.error("[certscore-mcp] create_scan is deprecated in the 0.2.x line. Use scan_site for new integrations.");
33027
33196
  }
33028
33197
  return toToolResult(await createPulseScanTool(input));
33029
33198
  } catch (error2) {
@@ -33036,12 +33205,36 @@ function createCertScoreMcpServer(options = {}) {
33036
33205
  toolContract("scan_site"),
33037
33206
  async (input) => {
33038
33207
  try {
33039
- return toToolResult(
33040
- await client.scans.create(input.url, {
33041
- freshness: input.freshness ?? "latest",
33042
- scanFrom: input.scanFrom
33043
- })
33044
- );
33208
+ const created = await client.scans.create(input.url, {
33209
+ freshness: input.freshness ?? "latest",
33210
+ scanFrom: input.scanFrom
33211
+ });
33212
+ if (input.waitForCompletion === false || created.type === "certscore_scan") {
33213
+ return toToolResult(created);
33214
+ }
33215
+ try {
33216
+ const completed = await client.scans.wait(created, {
33217
+ maxWaitMs: Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1e3 : DEFAULT_MCP_SCAN_WAIT_MS, DEFAULT_MCP_SCAN_WAIT_MS)
33218
+ });
33219
+ return toToolResult({
33220
+ ...completed,
33221
+ ...scanCreationMetadata(created),
33222
+ recommendedNextTool: "get_scan_bundle"
33223
+ });
33224
+ } catch (error2) {
33225
+ if (!(error2 instanceof CertScoreTimeoutError)) {
33226
+ throw error2;
33227
+ }
33228
+ const scanId = created.scanId ?? created.scan_id;
33229
+ if (scanId) {
33230
+ return toToolResult({
33231
+ ...await client.scans.status(scanId),
33232
+ ...scanCreationMetadata(created),
33233
+ recommendedNextTool: "get_scan_status"
33234
+ });
33235
+ }
33236
+ return toToolResult(created);
33237
+ }
33045
33238
  } catch (error2) {
33046
33239
  return toToolError(error2);
33047
33240
  }
@@ -33064,7 +33257,27 @@ function createCertScoreMcpServer(options = {}) {
33064
33257
  async ({ jobId, scanId }) => {
33065
33258
  try {
33066
33259
  if (scanId) {
33067
- return toToolResult(await client.scans.status(scanId));
33260
+ const status2 = await client.scans.status(scanId);
33261
+ const needsTerminalHydration = status2.status === "completed" || status2.status === "completed_limited";
33262
+ if (needsTerminalHydration) {
33263
+ try {
33264
+ const scan = await client.scans.get(scanId);
33265
+ return toToolResult({
33266
+ ...status2,
33267
+ type: "certscore_scan_job",
33268
+ status: scan.status,
33269
+ domain: scan.domain,
33270
+ resultDisposition: scan.resultDisposition,
33271
+ noGo: scan.noGo,
33272
+ startedAt: scan.startedAt,
33273
+ completedAt: scan.completedAt,
33274
+ scanTimeSeconds: scan.scanTimeSeconds,
33275
+ recommendedNextTool: "get_scan_bundle"
33276
+ });
33277
+ } catch {
33278
+ }
33279
+ }
33280
+ return toToolResult(status2);
33068
33281
  }
33069
33282
  if (!jobId) {
33070
33283
  return toToolResult({
@@ -33114,6 +33327,32 @@ function createCertScoreMcpServer(options = {}) {
33114
33327
  }
33115
33328
  }
33116
33329
  );
33330
+ registerTool(
33331
+ "get_scan_bundle",
33332
+ toolContract("get_scan_bundle"),
33333
+ async ({ scanId, maxFindings, maxPreConsentRows }) => {
33334
+ try {
33335
+ const [scan, report, evidence, findings, preConsentCookiesTrackers] = await Promise.all([
33336
+ client.scans.get(scanId),
33337
+ client.getScan(scanId, { detail: "summary", format: "json" }),
33338
+ client.getScan(scanId, { detail: "evidence", format: "json" }),
33339
+ client.findings.list(scanId),
33340
+ client.scans.preConsentCookiesTrackers(scanId)
33341
+ ]);
33342
+ return toToolResult(buildScanBundle({
33343
+ evidence,
33344
+ findings,
33345
+ maxFindings,
33346
+ maxPreConsentRows,
33347
+ preConsentCookiesTrackers,
33348
+ report,
33349
+ scan
33350
+ }));
33351
+ } catch (error2) {
33352
+ return toToolError(error2);
33353
+ }
33354
+ }
33355
+ );
33117
33356
  registerTool(
33118
33357
  "export_findings",
33119
33358
  toolContract("export_findings"),
@@ -33213,12 +33452,13 @@ async function main() {
33213
33452
  "",
33214
33453
  "Usage:",
33215
33454
  " certscore-mcp",
33216
- " certscore-mcp doctor"
33455
+ " certscore-mcp doctor",
33456
+ " certscore-mcp doctor --check-auth"
33217
33457
  ].join("\n"));
33218
33458
  return;
33219
33459
  }
33220
33460
  if (process.argv.includes("doctor")) {
33221
- const result = await getCertScoreMcpDoctorReport();
33461
+ const result = await getCertScoreMcpDoctorReport({ checkAuth: process.argv.includes("--check-auth") });
33222
33462
  console.log(result.lines.join("\n"));
33223
33463
  process.exitCode = result.exitCode;
33224
33464
  return;
@@ -33274,10 +33514,41 @@ async function getCertScoreMcpDoctorReport(options = {}) {
33274
33514
  }
33275
33515
  if (env.CERTSCORE_API_KEY?.trim()) {
33276
33516
  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.");
33517
+ if (options.checkAuth) {
33518
+ if (!healthUrl) {
33519
+ lines.push("[error] Cannot check credentials until CERTSCORE_BASE_URL is valid");
33520
+ exitCode = 1;
33521
+ } else {
33522
+ const authUrl = new URL("/api/v2/auth/check", healthUrl.origin);
33523
+ try {
33524
+ const response = await fetchImpl(authUrl, {
33525
+ headers: {
33526
+ accept: "application/json",
33527
+ authorization: `Bearer ${env.CERTSCORE_API_KEY.trim()}`
33528
+ },
33529
+ signal: AbortSignal.timeout(1e4)
33530
+ });
33531
+ if (response.ok) {
33532
+ lines.push(`[ok] API key authenticated at ${authUrl.href}`);
33533
+ } else {
33534
+ lines.push(`[error] API key rejected with HTTP ${response.status} at ${authUrl.href}`);
33535
+ exitCode = 1;
33536
+ }
33537
+ } catch (error2) {
33538
+ const message = error2 instanceof Error && error2.message ? error2.message : "request failed";
33539
+ lines.push(`[error] API key check failed at ${authUrl.href}: ${message}`);
33540
+ exitCode = 1;
33541
+ }
33542
+ }
33543
+ } else {
33544
+ lines.push("[info] Run certscore-mcp doctor --check-auth to verify the credential without creating a scan.");
33545
+ }
33278
33546
  } else {
33279
33547
  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.");
33548
+ 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.");
33549
+ if (options.checkAuth) {
33550
+ exitCode = 1;
33551
+ }
33281
33552
  }
33282
33553
  lines.push("CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.");
33283
33554
  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,7 +2,10 @@ 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;
8
+ toolProfile?: "full" | "light";
6
9
  }
7
10
  export declare function createCertScoreMcpServer(options?: CertScoreMcpOptions): McpServer;
8
11
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,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;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC;AAwDD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,mBAAwB,aA2SzE"}
package/dist/server.js CHANGED
@@ -1,9 +1,24 @@
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
+ upgradeSupportEmail: value.upgradeSupportEmail,
19
+ upgradeMessage: value.upgradeMessage
20
+ };
21
+ }
7
22
  function toolContract(name) {
8
23
  const contract = certScoreMcpToolContracts.find((candidate) => candidate.name === name);
9
24
  if (!contract) {
@@ -21,13 +36,23 @@ export function createCertScoreMcpServer(options = {}) {
21
36
  const client = new CertScoreClient({
22
37
  apiKey: options.apiKey,
23
38
  baseUrl: options.baseUrl,
39
+ clientName: "mcp",
40
+ forwardedClientIp: options.forwardedClientIp,
41
+ anonymousRequesterSecret: options.anonymousRequesterSecret,
24
42
  timeout: options.timeout
25
43
  });
26
44
  const server = new McpServer({
27
45
  name: "certscore",
28
46
  version: CERTSCORE_MCP_VERSION
29
47
  });
30
- const registerTool = server.registerTool.bind(server);
48
+ const lightTools = new Set(["scan_site", "get_scan_status", "get_scan_bundle"]);
49
+ const registerMcpTool = server.registerTool.bind(server);
50
+ const registerTool = (name, contract, handler) => {
51
+ if (options.toolProfile === "light" && !lightTools.has(name)) {
52
+ return;
53
+ }
54
+ registerMcpTool(name, contract, handler);
55
+ };
31
56
  async function createPulseScanTool(input) {
32
57
  const result = await client.submitScan(input.url, {
33
58
  detail: normalizeDetail(input.detail),
@@ -53,7 +78,7 @@ export function createCertScoreMcpServer(options = {}) {
53
78
  try {
54
79
  if (!createScanDeprecationWarningPrinted) {
55
80
  createScanDeprecationWarningPrinted = true;
56
- console.error("[certscore-mcp] create_scan is deprecated and will be removed in 0.2.0. Use scan_site.");
81
+ console.error("[certscore-mcp] create_scan is deprecated in the 0.2.x line. Use scan_site for new integrations.");
57
82
  }
58
83
  return toToolResult(await createPulseScanTool(input));
59
84
  }
@@ -63,10 +88,37 @@ export function createCertScoreMcpServer(options = {}) {
63
88
  });
64
89
  registerTool("scan_site", toolContract("scan_site"), async (input) => {
65
90
  try {
66
- return toToolResult(await client.scans.create(input.url, {
91
+ const created = await client.scans.create(input.url, {
67
92
  freshness: input.freshness ?? "latest",
68
93
  scanFrom: input.scanFrom
69
- }));
94
+ });
95
+ if (input.waitForCompletion === false || created.type === "certscore_scan") {
96
+ return toToolResult(created);
97
+ }
98
+ try {
99
+ const completed = await client.scans.wait(created, {
100
+ maxWaitMs: Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_WAIT_MS, DEFAULT_MCP_SCAN_WAIT_MS)
101
+ });
102
+ return toToolResult({
103
+ ...completed,
104
+ ...scanCreationMetadata(created),
105
+ recommendedNextTool: "get_scan_bundle"
106
+ });
107
+ }
108
+ catch (error) {
109
+ if (!(error instanceof CertScoreTimeoutError)) {
110
+ throw error;
111
+ }
112
+ const scanId = created.scanId ?? created.scan_id;
113
+ if (scanId) {
114
+ return toToolResult({
115
+ ...(await client.scans.status(scanId)),
116
+ ...scanCreationMetadata(created),
117
+ recommendedNextTool: "get_scan_status"
118
+ });
119
+ }
120
+ return toToolResult(created);
121
+ }
70
122
  }
71
123
  catch (error) {
72
124
  return toToolError(error);
@@ -83,7 +135,30 @@ export function createCertScoreMcpServer(options = {}) {
83
135
  registerTool("get_scan_status", toolContract("get_scan_status"), async ({ jobId, scanId }) => {
84
136
  try {
85
137
  if (scanId) {
86
- return toToolResult(await client.scans.status(scanId));
138
+ const status = await client.scans.status(scanId);
139
+ const needsTerminalHydration = status.status === "completed" || status.status === "completed_limited";
140
+ if (needsTerminalHydration) {
141
+ try {
142
+ const scan = await client.scans.get(scanId);
143
+ return toToolResult({
144
+ ...status,
145
+ type: "certscore_scan_job",
146
+ status: scan.status,
147
+ domain: scan.domain,
148
+ resultDisposition: scan.resultDisposition,
149
+ noGo: scan.noGo,
150
+ startedAt: scan.startedAt,
151
+ completedAt: scan.completedAt,
152
+ scanTimeSeconds: scan.scanTimeSeconds,
153
+ recommendedNextTool: "get_scan_bundle"
154
+ });
155
+ }
156
+ catch {
157
+ // Preserve the API status response if the terminal scan resource is
158
+ // briefly unavailable during eventual-consistency windows.
159
+ }
160
+ }
161
+ return toToolResult(status);
87
162
  }
88
163
  if (!jobId) {
89
164
  return toToolResult({
@@ -129,6 +204,29 @@ export function createCertScoreMcpServer(options = {}) {
129
204
  return toToolError(error);
130
205
  }
131
206
  });
207
+ registerTool("get_scan_bundle", toolContract("get_scan_bundle"), async ({ scanId, maxFindings, maxPreConsentRows }) => {
208
+ try {
209
+ const [scan, report, evidence, findings, preConsentCookiesTrackers] = await Promise.all([
210
+ client.scans.get(scanId),
211
+ client.getScan(scanId, { detail: "summary", format: "json" }),
212
+ client.getScan(scanId, { detail: "evidence", format: "json" }),
213
+ client.findings.list(scanId),
214
+ client.scans.preConsentCookiesTrackers(scanId)
215
+ ]);
216
+ return toToolResult(buildScanBundle({
217
+ evidence,
218
+ findings,
219
+ maxFindings,
220
+ maxPreConsentRows,
221
+ preConsentCookiesTrackers,
222
+ report,
223
+ scan
224
+ }));
225
+ }
226
+ catch (error) {
227
+ return toToolError(error);
228
+ }
229
+ });
132
230
  registerTool("export_findings", toolContract("export_findings"), async ({ scanId }) => {
133
231
  try {
134
232
  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.12",
4
4
  "mcpName": "ai.certscore/mcp",
5
5
  "private": false,
6
6
  "description": "MCP server for CertScore public website risk-signal workflows.",
@@ -31,7 +31,8 @@
31
31
  "dist",
32
32
  "README.md",
33
33
  "LICENSE",
34
- "server.json"
34
+ "server.json",
35
+ "server-light.json"
35
36
  ],
36
37
  "publishConfig": {
37
38
  "access": "public"
@@ -60,7 +61,7 @@
60
61
  },
61
62
  "devDependencies": {
62
63
  "@certscore/api-contracts": "0.1.0",
63
- "@certscore/sdk": "0.2.4"
64
+ "@certscore/sdk": "0.2.6"
64
65
  },
65
66
  "scripts": {
66
67
  "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",
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "ai.certscore/light",
4
+ "title": "CertScore Light",
5
+ "description": "No-account public website privacy risk scans with 20 new scans/day and free recent-result reuse.",
6
+ "status": "active",
7
+ "repository": {
8
+ "url": "https://github.com/ergoveritas1-alt/certscore.ai",
9
+ "source": "github"
10
+ },
11
+ "websiteUrl": "https://certscore.ai/developers/mcp",
12
+ "version": "0.2.12",
13
+ "remotes": [
14
+ {
15
+ "type": "streamable-http",
16
+ "url": "https://mcp.certscore.ai/mcp/light"
17
+ }
18
+ ]
19
+ }
package/server.json CHANGED
@@ -1,19 +1,26 @@
1
1
  {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "ai.certscore/mcp",
4
+ "title": "CertScore",
4
5
  "description": "MCP server for CertScore public website risk-signal workflows.",
5
6
  "status": "active",
6
7
  "repository": {
7
8
  "url": "https://github.com/ergoveritas1-alt/certscore.ai",
8
9
  "source": "github"
9
10
  },
10
- "version": "0.2.7",
11
+ "version": "0.2.12",
12
+ "remotes": [
13
+ {
14
+ "type": "streamable-http",
15
+ "url": "https://mcp.certscore.ai/mcp"
16
+ }
17
+ ],
11
18
  "packages": [
12
19
  {
13
20
  "registryType": "npm",
14
21
  "registryBaseUrl": "https://registry.npmjs.org",
15
22
  "identifier": "@certscore/mcp",
16
- "version": "0.2.7",
23
+ "version": "0.2.12",
17
24
  "transport": {
18
25
  "type": "stdio"
19
26
  },