@certscore/mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js ADDED
@@ -0,0 +1,329 @@
1
+ import { CertScoreError } from "@certscore/sdk";
2
+ const MAX_ERROR_RESPONSE_BODY_CHARS = 2_000;
3
+ export const MAX_EVIDENCE_PACKET_CHARS = 250_000;
4
+ const EVIDENCE_STRING_CHARS = 4_000;
5
+ const EVIDENCE_ARRAY_ITEMS = 40;
6
+ const EVIDENCE_OBJECT_KEYS = 80;
7
+ export function toToolResult(payload) {
8
+ const structuredContent = payload !== null && typeof payload === "object" && !Array.isArray(payload)
9
+ ? payload
10
+ : { value: payload };
11
+ return {
12
+ structuredContent,
13
+ content: [
14
+ {
15
+ type: "text",
16
+ text: JSON.stringify(payload)
17
+ }
18
+ ]
19
+ };
20
+ }
21
+ export function toToolError(error) {
22
+ if (error instanceof CertScoreError) {
23
+ return {
24
+ ...toToolResult({
25
+ error: {
26
+ name: error.name,
27
+ message: error.message,
28
+ status: error.status,
29
+ code: error.code,
30
+ retryAfterSeconds: "retryAfterSeconds" in error ? error.retryAfterSeconds : undefined,
31
+ responseBody: truncateErrorResponseBody(error.responseBody)
32
+ }
33
+ }),
34
+ isError: true
35
+ };
36
+ }
37
+ return {
38
+ ...toToolResult({
39
+ error: {
40
+ name: error instanceof Error ? error.name : "Error",
41
+ message: error instanceof Error ? error.message : "Unknown CertScore MCP error."
42
+ }
43
+ }),
44
+ isError: true
45
+ };
46
+ }
47
+ export function boundEvidencePacket(payload, maxSerializedChars = MAX_EVIDENCE_PACKET_CHARS) {
48
+ const originalSerializedChars = measureSerializedChars(payload);
49
+ if (originalSerializedChars <= maxSerializedChars) {
50
+ return payload;
51
+ }
52
+ const compacted = withMcpMetadata(compactEvidenceValue(payload, {
53
+ arrayItems: EVIDENCE_ARRAY_ITEMS,
54
+ depth: 8,
55
+ objectKeys: EVIDENCE_OBJECT_KEYS,
56
+ stringChars: EVIDENCE_STRING_CHARS
57
+ }), {
58
+ maxSerializedChars,
59
+ originalSerializedChars,
60
+ strategy: "compact_nested_values",
61
+ truncated: true
62
+ });
63
+ if (measureSerializedChars(compacted) <= maxSerializedChars) {
64
+ return compacted;
65
+ }
66
+ const minimal = withMcpMetadata(minimalEvidencePacket(payload), {
67
+ maxSerializedChars,
68
+ originalSerializedChars,
69
+ strategy: "minimal_safe_summary",
70
+ truncated: true
71
+ });
72
+ if (measureSerializedChars(minimal) <= maxSerializedChars) {
73
+ return minimal;
74
+ }
75
+ return {
76
+ type: "certscore_mcp_evidence_packet",
77
+ scanId: extractScanId(payload),
78
+ summary: "Evidence packet was too large for MCP transport and was reduced to metadata. Fetch API v2 directly for the full public-safe artifact.",
79
+ mcpMetadata: {
80
+ maxSerializedChars,
81
+ originalSerializedChars,
82
+ strategy: "metadata_only",
83
+ truncated: true
84
+ }
85
+ };
86
+ }
87
+ function measureSerializedChars(value) {
88
+ return JSON.stringify(value)?.length ?? 0;
89
+ }
90
+ function compactEvidenceValue(value, options) {
91
+ if (value === null || value === undefined) {
92
+ return value;
93
+ }
94
+ if (typeof value === "string") {
95
+ return value.length > options.stringChars ? `${value.slice(0, options.stringChars)}…[truncated]` : value;
96
+ }
97
+ if (typeof value !== "object") {
98
+ return value;
99
+ }
100
+ if (options.depth <= 0) {
101
+ return "[truncated: depth limit]";
102
+ }
103
+ if (Array.isArray(value)) {
104
+ const items = value.slice(0, options.arrayItems).map((item) => compactEvidenceValue(item, {
105
+ ...options,
106
+ depth: options.depth - 1
107
+ }));
108
+ if (value.length > options.arrayItems) {
109
+ items.push({
110
+ omittedItems: value.length - options.arrayItems,
111
+ truncated: true
112
+ });
113
+ }
114
+ return items;
115
+ }
116
+ const entries = Object.entries(value);
117
+ const next = {};
118
+ for (const [key, nested] of entries.slice(0, options.objectKeys)) {
119
+ next[key] = compactEvidenceValue(nested, {
120
+ ...options,
121
+ depth: options.depth - 1
122
+ });
123
+ }
124
+ if (entries.length > options.objectKeys) {
125
+ next.__truncatedKeys = entries.length - options.objectKeys;
126
+ }
127
+ return next;
128
+ }
129
+ function withMcpMetadata(value, metadata) {
130
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
131
+ const record = value;
132
+ const existing = record.mcpMetadata !== null && typeof record.mcpMetadata === "object" && !Array.isArray(record.mcpMetadata)
133
+ ? record.mcpMetadata
134
+ : {};
135
+ return {
136
+ ...record,
137
+ mcpMetadata: {
138
+ ...existing,
139
+ ...metadata
140
+ }
141
+ };
142
+ }
143
+ return {
144
+ type: "certscore_mcp_evidence_packet",
145
+ value,
146
+ mcpMetadata: metadata
147
+ };
148
+ }
149
+ function minimalEvidencePacket(payload) {
150
+ const record = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
151
+ return {
152
+ type: typeof record.type === "string" ? record.type : "certscore_mcp_evidence_packet",
153
+ scanId: extractScanId(record),
154
+ scan_id: typeof record.scan_id === "string" ? record.scan_id : undefined,
155
+ domain: typeof record.domain === "string" ? record.domain : null,
156
+ summary: compactEvidenceValue(record.summary ?? null, {
157
+ arrayItems: 20,
158
+ depth: 4,
159
+ objectKeys: 30,
160
+ stringChars: 1_000
161
+ }),
162
+ findings: compactEvidenceValue(record.findings ?? record.topFindings ?? [], {
163
+ arrayItems: 20,
164
+ depth: 5,
165
+ objectKeys: 40,
166
+ stringChars: 1_000
167
+ }),
168
+ evidenceHighlights: compactEvidenceValue(record.evidenceHighlights ?? null, {
169
+ arrayItems: 20,
170
+ depth: 5,
171
+ objectKeys: 40,
172
+ stringChars: 1_000
173
+ }),
174
+ coverage: compactEvidenceValue(record.coverage ?? null, {
175
+ arrayItems: 20,
176
+ depth: 4,
177
+ objectKeys: 30,
178
+ stringChars: 1_000
179
+ }),
180
+ disclaimer: typeof record.disclaimer === "string" ? record.disclaimer : null
181
+ };
182
+ }
183
+ function extractScanId(payload) {
184
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
185
+ return null;
186
+ }
187
+ const record = payload;
188
+ if (typeof record.scanId === "string") {
189
+ return record.scanId;
190
+ }
191
+ if (typeof record.scan_id === "string") {
192
+ return record.scan_id;
193
+ }
194
+ const scan = record.scan;
195
+ const nestedScanId = scan !== null && typeof scan === "object" && !Array.isArray(scan)
196
+ ? scan.scanId
197
+ : null;
198
+ if (typeof nestedScanId === "string") {
199
+ return nestedScanId;
200
+ }
201
+ return null;
202
+ }
203
+ function truncateErrorResponseBody(value) {
204
+ if (typeof value !== "string") {
205
+ const serialized = JSON.stringify(value);
206
+ if (serialized === undefined || serialized.length <= MAX_ERROR_RESPONSE_BODY_CHARS) {
207
+ return value;
208
+ }
209
+ return `${serialized.slice(0, MAX_ERROR_RESPONSE_BODY_CHARS)}…[truncated]`;
210
+ }
211
+ if (value.length <= MAX_ERROR_RESPONSE_BODY_CHARS) {
212
+ return value;
213
+ }
214
+ return `${value.slice(0, MAX_ERROR_RESPONSE_BODY_CHARS)}…[truncated]`;
215
+ }
216
+ export function normalizeDetail(detail) {
217
+ return detail ?? "summary";
218
+ }
219
+ export function normalizeFormat(format) {
220
+ return format ?? "json";
221
+ }
222
+ export function scanIdFromStatus(status) {
223
+ return status.scanId ?? status.scan_id ?? null;
224
+ }
225
+ export function scanIdFromPulse(report) {
226
+ const nestedScan = "scan" in report && report.scan && typeof report.scan === "object" ? report.scan : undefined;
227
+ return report.scanId ?? report.scan_id ?? nestedScan?.scanId ?? null;
228
+ }
229
+ export function findingsFromReport(report) {
230
+ const findings = Array.isArray(report.findings) ? report.findings : [];
231
+ const topFindings = Array.isArray(report.topFindings) ? report.topFindings : [];
232
+ const byId = new Map();
233
+ for (const finding of [...findings, ...topFindings]) {
234
+ byId.set(finding.id, finding);
235
+ }
236
+ return [...byId.values()];
237
+ }
238
+ export function exportFindings(report) {
239
+ return {
240
+ type: "certscore_mcp_findings_export",
241
+ scanId: scanIdFromPulse(report),
242
+ domain: report.domain ?? report.request?.domain ?? null,
243
+ summary: report.summary ?? null,
244
+ findings: findingsFromReport(report).map((finding) => ({
245
+ id: finding.id,
246
+ label: finding.label ?? null,
247
+ criticality: finding.criticality ?? null,
248
+ confidence: finding.confidence ?? null,
249
+ plainEnglish: finding.plainEnglish ?? null,
250
+ evidenceDigest: finding.evidenceDigest ?? null,
251
+ evidenceSummary: finding.evidence?.summary ?? null,
252
+ reviewLenses: finding.reviewLenses ?? [],
253
+ anchorUrl: finding.anchorUrl ?? finding.evidence?.fullEvidenceUrl ?? null,
254
+ nextStep: finding.nextStep ?? null
255
+ })),
256
+ disclaimer: report.disclaimer ?? null
257
+ };
258
+ }
259
+ export function paginateFindingList(payload, options = {}) {
260
+ const findings = Array.isArray(payload.findings) ? payload.findings : null;
261
+ if (!findings) {
262
+ return payload;
263
+ }
264
+ const offset = Math.max(0, options.offset ?? 0);
265
+ const limit = Math.min(200, Math.max(1, options.limit ?? 50));
266
+ const paginatedFindings = findings.slice(offset, offset + limit);
267
+ const next = {
268
+ ...payload,
269
+ findings: paginatedFindings,
270
+ pagination: {
271
+ limit,
272
+ offset,
273
+ returned: paginatedFindings.length,
274
+ total: findings.length,
275
+ truncated: offset + limit < findings.length
276
+ }
277
+ };
278
+ return next;
279
+ }
280
+ export function limitPreConsentRows(payload, options = {}) {
281
+ const rows = Array.isArray(payload.rows) ? payload.rows : null;
282
+ if (!rows) {
283
+ return payload;
284
+ }
285
+ const maxRows = Math.min(200, Math.max(1, options.maxRows ?? 200));
286
+ const total = rows.length;
287
+ const truncated = total > maxRows;
288
+ return {
289
+ ...payload,
290
+ rows: rows.slice(0, maxRows),
291
+ summary: {
292
+ ...(payload.summary && typeof payload.summary === "object" && !Array.isArray(payload.summary) ? payload.summary : {}),
293
+ totalRowCount: total,
294
+ truncated
295
+ }
296
+ };
297
+ }
298
+ export function explainFinding(report, findingId) {
299
+ const finding = findingsFromReport(report).find((candidate) => candidate.id === findingId);
300
+ if (!finding) {
301
+ return {
302
+ type: "certscore_mcp_finding_explanation",
303
+ scanId: scanIdFromPulse(report),
304
+ findingId,
305
+ found: false,
306
+ message: "Finding was not present in this Pulse report.",
307
+ availableFindingIds: findingsFromReport(report).map((candidate) => candidate.id),
308
+ disclaimer: report.disclaimer ?? null
309
+ };
310
+ }
311
+ return {
312
+ type: "certscore_mcp_finding_explanation",
313
+ scanId: scanIdFromPulse(report),
314
+ findingId,
315
+ found: true,
316
+ label: finding.label ?? finding.id,
317
+ criticality: finding.criticality ?? null,
318
+ confidence: finding.confidence ?? null,
319
+ plainEnglish: finding.plainEnglish ?? null,
320
+ evidenceSummary: finding.evidence?.summary ?? null,
321
+ evidenceDigest: finding.evidenceDigest ?? null,
322
+ exampleEvents: finding.evidence?.exampleEvents ?? [],
323
+ reviewLenses: finding.reviewLenses ?? [],
324
+ anchorUrl: finding.anchorUrl ?? finding.evidence?.fullEvidenceUrl ?? null,
325
+ nextStep: finding.nextStep ?? null,
326
+ caveats: report.coverage?.limitations ?? [],
327
+ disclaimer: report.disclaimer ?? null
328
+ };
329
+ }
@@ -0,0 +1,2 @@
1
+ export declare const CERTSCORE_MCP_VERSION: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,qBAAqB,QAAsB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { readFileSync } from "node:fs";
2
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
3
+ export const CERTSCORE_MCP_VERSION = packageJson.version;
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@certscore/mcp",
3
+ "version": "0.2.0",
4
+ "mcpName": "ai.certscore/mcp",
5
+ "private": false,
6
+ "description": "MCP server for CertScore public website risk-signal workflows.",
7
+ "type": "module",
8
+ "main": "./dist/certscore-mcp.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "bin": {
11
+ "certscore-mcp": "dist/certscore-mcp.mjs"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/certscore-mcp.mjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE",
26
+ "server.json"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/ergoveritas1-alt/certscore.ai.git",
34
+ "directory": "packages/certscore-mcp"
35
+ },
36
+ "homepage": "https://certscore.ai/developers/mcp",
37
+ "bugs": {
38
+ "url": "https://certscore.ai/contact",
39
+ "email": "support@certscore.ai"
40
+ },
41
+ "scripts": {
42
+ "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",
43
+ "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --banner:js='#!/usr/bin/env node' --outfile=dist/certscore-mcp.mjs",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json",
45
+ "test": "node --import tsx --test src/*.test.ts",
46
+ "prepack": "pnpm run build",
47
+ "clean": "rm -rf dist"
48
+ },
49
+ "keywords": [
50
+ "certscore",
51
+ "pulse",
52
+ "mcp",
53
+ "modelcontextprotocol",
54
+ "model-context-protocol"
55
+ ],
56
+ "license": "proprietary",
57
+ "dependencies": {
58
+ "@modelcontextprotocol/sdk": "^1.29.0",
59
+ "zod": "^3.25.76"
60
+ },
61
+ "devDependencies": {
62
+ "@certscore/api-contracts": "workspace:*",
63
+ "@certscore/sdk": "workspace:*"
64
+ }
65
+ }
package/server.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "ai.certscore/mcp",
4
+ "title": "CertScore MCP",
5
+ "description": "MCP server for CertScore public website risk-signal workflows, including scan creation, status checks, findings, bounded evidence, and latest-domain lookups. CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.",
6
+ "repository": {
7
+ "url": "https://github.com/ergoveritas1-alt/certscore.ai",
8
+ "source": "github"
9
+ },
10
+ "version": "0.2.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@certscore/mcp",
15
+ "version": "0.2.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "CERTSCORE_API_KEY",
22
+ "description": "CertScore API key with scan:read, scan:create, and mcp scopes.",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ },
27
+ {
28
+ "name": "CERTSCORE_BASE_URL",
29
+ "description": "Optional CertScore API base URL. Defaults to https://certscore.ai.",
30
+ "isRequired": false,
31
+ "format": "uri",
32
+ "isSecret": false
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }