@certscore/mcp 0.2.15 → 0.2.16
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 +40 -22
- package/dist/certscore-mcp.mjs +3775 -4125
- package/dist/index.js +1 -1
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +336 -79
- package/dist/tools.d.ts +23 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +856 -80
- package/package.json +1 -1
- package/server-light.json +19 -4
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -141,7 +141,7 @@ export async function getCertScoreMcpDoctorReport(options = {}) {
|
|
|
141
141
|
exitCode = 1;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
lines.push("CertScore outputs are automated public-web observations for review, not legal advice, certification, or a compliance determination.");
|
|
144
|
+
lines.push("CertScore outputs are automated public-web observations for human and agentic review, not legal advice, certification, or a compliance determination.");
|
|
145
145
|
return { exitCode, lines };
|
|
146
146
|
}
|
|
147
147
|
function isMainModule() {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { RequestInfo } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
export interface CertScoreMcpOptions {
|
|
3
4
|
apiKey?: string;
|
|
4
5
|
baseUrl?: string;
|
|
5
6
|
forwardedClientIp?: string | null;
|
|
7
|
+
resolveForwardedClientIp?: (headers: RequestInfo["headers"]) => string | null;
|
|
8
|
+
resolveAnonymousRequesterSession?: () => string | null;
|
|
6
9
|
anonymousRequesterSecret?: string | null;
|
|
10
|
+
anonymousSurface?: "mcp_light" | "mcp_anonymous" | null;
|
|
7
11
|
timeout?: number;
|
|
8
12
|
toolProfile?: "full" | "light";
|
|
13
|
+
exampleDomainDemoUrl?: string | null;
|
|
14
|
+
onToolInvocation?: (observation: McpToolInvocationObservation, requestContext: McpToolInvocationRequestContext) => void | Promise<void>;
|
|
9
15
|
}
|
|
16
|
+
export type McpToolInvocationRequestContext = {
|
|
17
|
+
headers: RequestInfo["headers"] | null;
|
|
18
|
+
};
|
|
19
|
+
export type McpToolInvocationObservation = {
|
|
20
|
+
durationMs: number;
|
|
21
|
+
errorCode: string | null;
|
|
22
|
+
freshness: "latest" | "refresh" | null;
|
|
23
|
+
isCanary: boolean;
|
|
24
|
+
outcome: "success" | "error" | "rate_limited";
|
|
25
|
+
quotaOutcome: "allowed" | "rate_limited";
|
|
26
|
+
requestedResource: string | null;
|
|
27
|
+
requestedResourceType: "url" | "domain" | "scan_id" | "job_id" | null;
|
|
28
|
+
scanDecision: "reused" | "new" | "unavailable" | "not_applicable";
|
|
29
|
+
scanFrom: "eu_de" | "eu_ie" | "california" | null;
|
|
30
|
+
scanId: string | null;
|
|
31
|
+
scanStatus: string | null;
|
|
32
|
+
targetHostname: string | null;
|
|
33
|
+
toolName: string;
|
|
34
|
+
transportOutcome: "mcp_result" | "mcp_error";
|
|
35
|
+
};
|
|
36
|
+
export declare function resolveMcpScanSiteWaitBudget(input: {
|
|
37
|
+
maxWaitSeconds?: number;
|
|
38
|
+
startedAtMs: number;
|
|
39
|
+
nowMs: number;
|
|
40
|
+
}): {
|
|
41
|
+
elapsedMs: number;
|
|
42
|
+
remainingWaitMs: number;
|
|
43
|
+
responseReserveMs: number;
|
|
44
|
+
totalBudgetMs: number;
|
|
45
|
+
};
|
|
46
|
+
export declare function projectMcpToolInvocationObservation(input: {
|
|
47
|
+
args: unknown;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
result: unknown;
|
|
50
|
+
toolName: string;
|
|
51
|
+
}): McpToolInvocationObservation;
|
|
10
52
|
export declare function createCertScoreMcpServer(options?: CertScoreMcpOptions): McpServer;
|
|
11
53
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAItE,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,CAAC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,KAAK,MAAM,GAAG,IAAI,CAAC;IAC9E,gCAAgC,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACvD,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,gBAAgB,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,gBAAgB,CAAC,EAAE,CACjB,WAAW,EAAE,4BAA4B,EACzC,cAAc,EAAE,+BAA+B,KAC5C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAID,MAAM,MAAM,+BAA+B,GAAG;IAC5C,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;CACxC,CAAC;AA4BF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;IAC9C,YAAY,EAAE,SAAS,GAAG,cAAc,CAAC;IACzC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,qBAAqB,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;IACtE,YAAY,EAAE,QAAQ,GAAG,KAAK,GAAG,aAAa,GAAG,gBAAgB,CAAC;IAClE,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,YAAY,GAAG,IAAI,CAAC;IAClD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,YAAY,GAAG,WAAW,CAAC;CAC9C,CAAC;AAOF,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;;;;;EAYA;AAoJD,wBAAgB,mCAAmC,CAAC,KAAK,EAAE;IACzD,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,4BAA4B,CAqD/B;AAkBD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,mBAAwB,aA8XzE"}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
|
-
import { CertScoreClient
|
|
2
|
-
import { certScoreMcpToolContracts } from "@certscore/api-contracts";
|
|
1
|
+
import { CertScoreClient } from "@certscore/sdk";
|
|
2
|
+
import { certScoreMcpToolContracts, isCanonicalScanId } 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, buildScanBundle, exportFindings, limitPreConsentRows, normalizeDetail, normalizeFormat, paginateFindingList, toInvalidArgumentsToolError, toToolError, toToolResult, withMcpAgentGuidance } from "./tools.js";
|
|
6
|
-
const
|
|
5
|
+
import { boundEvidencePacket, buildScanBundle, exportFindings, findingListText, limitPreConsentRows, markdownReportText, MAX_EVIDENCE_PACKET_CHARS, normalizeDetail, normalizeFormat, paginateFindingList, preConsentInventoryText, pulseReportText, scanBundleText, scanStatusText, toInvalidArgumentsToolError, toInvalidScanIdToolError, toToolError, toToolResult, withMcpAgentGuidance, withMcpScanProvenanceGuidance } from "./tools.js";
|
|
6
|
+
const DEFAULT_MCP_SCAN_TOOL_BUDGET_MS = 25_000;
|
|
7
|
+
const MAX_MCP_SCAN_TOOL_BUDGET_MS = 45_000;
|
|
8
|
+
const MCP_SCAN_TOOL_RESPONSE_RESERVE_MS = 1_000;
|
|
9
|
+
const LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES = 25_000;
|
|
10
|
+
export function resolveMcpScanSiteWaitBudget(input) {
|
|
11
|
+
const totalBudgetMs = Math.min(input.maxWaitSeconds ? input.maxWaitSeconds * 1_000 : DEFAULT_MCP_SCAN_TOOL_BUDGET_MS, MAX_MCP_SCAN_TOOL_BUDGET_MS);
|
|
12
|
+
const elapsedMs = Math.max(0, input.nowMs - input.startedAtMs);
|
|
13
|
+
return {
|
|
14
|
+
elapsedMs,
|
|
15
|
+
remainingWaitMs: Math.max(0, totalBudgetMs - elapsedMs - MCP_SCAN_TOOL_RESPONSE_RESERVE_MS),
|
|
16
|
+
responseReserveMs: MCP_SCAN_TOOL_RESPONSE_RESERVE_MS,
|
|
17
|
+
totalBudgetMs,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function exampleDomainDemoSubstitution(requestedUrl, demoUrl) {
|
|
21
|
+
if (!demoUrl)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(requestedUrl.includes("://") ? requestedUrl : `https://${requestedUrl}`);
|
|
25
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
26
|
+
const reserved = ["example.com", "example.net", "example.org"].some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
27
|
+
if (!reserved)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
requestedUrl,
|
|
31
|
+
effectiveUrl: demoUrl,
|
|
32
|
+
reason: "iana_example_domain",
|
|
33
|
+
message: "The requested IANA example domain is a documentation placeholder, so CertScore scanned its controlled demonstration site instead. Findings describe the effective URL, not the requested placeholder."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function withExampleDomainDemo(value, substitution) {
|
|
41
|
+
return substitution ? { ...value, demoSubstitution: substitution } : value;
|
|
42
|
+
}
|
|
43
|
+
function exampleDomainDemoText(value, substitution) {
|
|
44
|
+
if (!substitution)
|
|
45
|
+
return undefined;
|
|
46
|
+
const status = typeof value.status === "string" ? ` Status=${value.status}.` : "";
|
|
47
|
+
const scanId = typeof value.scanId === "string" ? ` ScanId=${value.scanId}.` : "";
|
|
48
|
+
return `${substitution.message}${status}${scanId} Full result and substitution provenance are in structuredContent.`;
|
|
49
|
+
}
|
|
7
50
|
async function retryTransientOriginFailure(operation) {
|
|
8
51
|
try {
|
|
9
52
|
return await operation();
|
|
@@ -43,15 +86,161 @@ function toolContract(name) {
|
|
|
43
86
|
annotations: contract.annotations
|
|
44
87
|
};
|
|
45
88
|
}
|
|
89
|
+
function boundedTelemetryToken(value, maxLength) {
|
|
90
|
+
return typeof value === "string" && /^[a-zA-Z0-9_.:-]+$/.test(value) && value.length <= maxLength
|
|
91
|
+
? value
|
|
92
|
+
: null;
|
|
93
|
+
}
|
|
94
|
+
function telemetryResultRecord(result) {
|
|
95
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
96
|
+
return {};
|
|
97
|
+
const toolResult = result;
|
|
98
|
+
if (toolResult.structuredContent && typeof toolResult.structuredContent === "object" && !Array.isArray(toolResult.structuredContent)) {
|
|
99
|
+
return toolResult.structuredContent;
|
|
100
|
+
}
|
|
101
|
+
const firstText = Array.isArray(toolResult.content)
|
|
102
|
+
? toolResult.content.find((item) => item && typeof item === "object" && item.type === "text")
|
|
103
|
+
: null;
|
|
104
|
+
if (!firstText || typeof firstText.text !== "string")
|
|
105
|
+
return {};
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(firstText.text);
|
|
108
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function telemetryHostname(value) {
|
|
115
|
+
if (typeof value !== "string")
|
|
116
|
+
return null;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
119
|
+
return parsed.hostname.toLowerCase().replace(/\.$/, "").slice(0, 253) || null;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function telemetryUrl(value) {
|
|
126
|
+
if (typeof value !== "string")
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
130
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
131
|
+
return null;
|
|
132
|
+
return parsed.origin.slice(0, 512);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function requestedTelemetryResource(args) {
|
|
139
|
+
const scanId = boundedTelemetryToken(args.scanId, 128);
|
|
140
|
+
if (scanId)
|
|
141
|
+
return { requestedResource: scanId, requestedResourceType: "scan_id" };
|
|
142
|
+
const jobId = boundedTelemetryToken(args.jobId, 128);
|
|
143
|
+
if (jobId)
|
|
144
|
+
return { requestedResource: jobId, requestedResourceType: "job_id" };
|
|
145
|
+
const url = telemetryUrl(args.url);
|
|
146
|
+
if (url)
|
|
147
|
+
return { requestedResource: url, requestedResourceType: "url" };
|
|
148
|
+
const domain = telemetryHostname(args.domain);
|
|
149
|
+
if (domain)
|
|
150
|
+
return { requestedResource: domain, requestedResourceType: "domain" };
|
|
151
|
+
return { requestedResource: null, requestedResourceType: null };
|
|
152
|
+
}
|
|
153
|
+
function isCertScoreCanaryUrl(value) {
|
|
154
|
+
if (typeof value !== "string")
|
|
155
|
+
return false;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
|
|
158
|
+
return parsed.pathname.startsWith("/.well-known/certscore-canary/");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function projectMcpToolInvocationObservation(input) {
|
|
165
|
+
const args = input.args && typeof input.args === "object" && !Array.isArray(input.args)
|
|
166
|
+
? input.args
|
|
167
|
+
: {};
|
|
168
|
+
const result = telemetryResultRecord(input.result);
|
|
169
|
+
const error = result.error && typeof result.error === "object" && !Array.isArray(result.error)
|
|
170
|
+
? result.error
|
|
171
|
+
: null;
|
|
172
|
+
const errorCode = boundedTelemetryToken(error?.code ?? result.errorCode, 100);
|
|
173
|
+
const rateLimited = errorCode === "rate_limited" || result.status === "rate_limited";
|
|
174
|
+
const isError = Boolean(input.result?.isError) || Boolean(error);
|
|
175
|
+
const outcome = rateLimited ? "rate_limited" : isError ? "error" : "success";
|
|
176
|
+
const resultScanId = boundedTelemetryToken(result.scanId ?? result.scan_id ?? result.jobId, 128);
|
|
177
|
+
const inputScanId = boundedTelemetryToken(args.scanId, 128);
|
|
178
|
+
const requestedResource = requestedTelemetryResource(args);
|
|
179
|
+
const targetHostname = input.toolName === "certscore_scan_site"
|
|
180
|
+
? telemetryHostname(args.url)
|
|
181
|
+
: input.toolName === "certscore_get_latest_domain_scan"
|
|
182
|
+
|| input.toolName === "certscore_get_latest_domain_pre_consent_cookies_trackers"
|
|
183
|
+
? telemetryHostname(args.domain)
|
|
184
|
+
: null;
|
|
185
|
+
const isCanary = isCertScoreCanaryUrl(args.url);
|
|
186
|
+
const executionMode = result.executionMode;
|
|
187
|
+
const scanDecision = input.toolName !== "certscore_scan_site"
|
|
188
|
+
? "not_applicable"
|
|
189
|
+
: outcome !== "success"
|
|
190
|
+
? "unavailable"
|
|
191
|
+
: result.reused === true || executionMode === "reused_scan"
|
|
192
|
+
? "reused"
|
|
193
|
+
: result.reused === false || executionMode === "new_scan" || result.quotaConsumed === true
|
|
194
|
+
? "new"
|
|
195
|
+
: "unavailable";
|
|
196
|
+
return {
|
|
197
|
+
durationMs: Math.max(0, Math.min(Math.round(input.durationMs), 3_600_000)),
|
|
198
|
+
errorCode: rateLimited ? "rate_limited" : errorCode,
|
|
199
|
+
freshness: args.freshness === "refresh" ? "refresh" : input.toolName === "certscore_scan_site" ? "latest" : null,
|
|
200
|
+
isCanary,
|
|
201
|
+
outcome,
|
|
202
|
+
quotaOutcome: rateLimited ? "rate_limited" : "allowed",
|
|
203
|
+
...requestedResource,
|
|
204
|
+
scanDecision,
|
|
205
|
+
scanFrom: args.scanFrom === "eu_de" || args.scanFrom === "eu_ie" || args.scanFrom === "california"
|
|
206
|
+
? args.scanFrom
|
|
207
|
+
: result.scanFrom === "eu_de" || result.scanFrom === "eu_ie" || result.scanFrom === "california"
|
|
208
|
+
? result.scanFrom
|
|
209
|
+
: null,
|
|
210
|
+
scanId: resultScanId ?? inputScanId,
|
|
211
|
+
scanStatus: boundedTelemetryToken(result.status, 64),
|
|
212
|
+
targetHostname,
|
|
213
|
+
toolName: input.toolName,
|
|
214
|
+
transportOutcome: isError ? "mcp_error" : "mcp_result",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function observeToolInvocation(observer, observation, requestContext) {
|
|
218
|
+
if (!observer)
|
|
219
|
+
return;
|
|
220
|
+
queueMicrotask(() => {
|
|
221
|
+
Promise.resolve().then(() => observer(observation, requestContext)).catch((error) => {
|
|
222
|
+
console.error("[certscore-mcp] telemetry observer failed", {
|
|
223
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
224
|
+
toolName: observation.toolName,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
46
229
|
export function createCertScoreMcpServer(options = {}) {
|
|
47
|
-
const
|
|
230
|
+
const createClient = (forwardedClientIp, anonymousRequesterSession) => new CertScoreClient({
|
|
48
231
|
apiKey: options.apiKey,
|
|
49
232
|
baseUrl: options.baseUrl,
|
|
50
233
|
clientName: "mcp",
|
|
51
|
-
forwardedClientIp
|
|
234
|
+
forwardedClientIp,
|
|
52
235
|
anonymousRequesterSecret: options.anonymousRequesterSecret,
|
|
236
|
+
anonymousSurface: options.anonymousSurface,
|
|
237
|
+
anonymousRequesterSession,
|
|
53
238
|
timeout: options.timeout
|
|
54
239
|
});
|
|
240
|
+
const client = createClient(options.forwardedClientIp, options.resolveAnonymousRequesterSession?.());
|
|
241
|
+
const clientForRequest = (extra) => options.resolveForwardedClientIp
|
|
242
|
+
? createClient(options.resolveForwardedClientIp(extra.requestInfo?.headers ?? {}), options.resolveAnonymousRequesterSession?.())
|
|
243
|
+
: client;
|
|
55
244
|
const server = new McpServer({
|
|
56
245
|
name: "certscore",
|
|
57
246
|
version: CERTSCORE_MCP_VERSION
|
|
@@ -61,30 +250,100 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
61
250
|
? toInvalidArgumentsToolError(message)
|
|
62
251
|
: sdkCreateToolError(message);
|
|
63
252
|
const lightTools = new Set(["certscore_scan_site", "certscore_get_scan_status", "certscore_get_scan_bundle"]);
|
|
253
|
+
const scanIdTools = new Set([
|
|
254
|
+
"certscore_explain_finding",
|
|
255
|
+
"certscore_export_findings",
|
|
256
|
+
"certscore_get_evidence",
|
|
257
|
+
"certscore_get_pre_consent_cookies_trackers",
|
|
258
|
+
"certscore_get_report",
|
|
259
|
+
"certscore_get_scan",
|
|
260
|
+
"certscore_get_scan_bundle",
|
|
261
|
+
"certscore_get_scan_status",
|
|
262
|
+
"certscore_list_findings",
|
|
263
|
+
]);
|
|
64
264
|
const registerMcpTool = server.registerTool.bind(server);
|
|
65
265
|
const registerTool = (name, contract, handler) => {
|
|
66
266
|
if (options.toolProfile === "light" && !lightTools.has(name)) {
|
|
67
267
|
return;
|
|
68
268
|
}
|
|
69
|
-
|
|
269
|
+
const typedHandler = handler;
|
|
270
|
+
registerMcpTool(name, contract, async (input, extra) => {
|
|
271
|
+
const startedAt = Date.now();
|
|
272
|
+
try {
|
|
273
|
+
const scanId = input && typeof input === "object" && !Array.isArray(input)
|
|
274
|
+
? input.scanId
|
|
275
|
+
: null;
|
|
276
|
+
const result = scanIdTools.has(name) && !isCanonicalScanId(scanId)
|
|
277
|
+
? toInvalidScanIdToolError()
|
|
278
|
+
: await typedHandler(input, extra);
|
|
279
|
+
observeToolInvocation(options.onToolInvocation, projectMcpToolInvocationObservation({
|
|
280
|
+
args: input,
|
|
281
|
+
durationMs: Date.now() - startedAt,
|
|
282
|
+
result,
|
|
283
|
+
toolName: name,
|
|
284
|
+
}), { headers: extra.requestInfo?.headers ?? null });
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
observeToolInvocation(options.onToolInvocation, {
|
|
289
|
+
...projectMcpToolInvocationObservation({
|
|
290
|
+
args: input,
|
|
291
|
+
durationMs: Date.now() - startedAt,
|
|
292
|
+
result: { isError: true, structuredContent: { error: { code: "handler_exception" } } },
|
|
293
|
+
toolName: name,
|
|
294
|
+
}),
|
|
295
|
+
errorCode: "handler_exception",
|
|
296
|
+
outcome: "error",
|
|
297
|
+
transportOutcome: "mcp_error",
|
|
298
|
+
}, { headers: extra.requestInfo?.headers ?? null });
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
70
302
|
};
|
|
71
|
-
registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input) => {
|
|
303
|
+
registerTool("certscore_scan_site", toolContract("certscore_scan_site"), async (input, extra) => {
|
|
304
|
+
const toolStartedAtMs = Date.now();
|
|
305
|
+
const client = clientForRequest(extra);
|
|
306
|
+
const demoSubstitution = exampleDomainDemoSubstitution(input.url, options.exampleDomainDemoUrl);
|
|
307
|
+
const effectiveUrl = demoSubstitution?.effectiveUrl ?? input.url;
|
|
72
308
|
try {
|
|
73
|
-
const created = await client.scans.create(
|
|
309
|
+
const created = await client.scans.create(effectiveUrl, {
|
|
74
310
|
freshness: input.freshness ?? "latest",
|
|
75
311
|
scanFrom: input.scanFrom
|
|
76
312
|
});
|
|
77
313
|
if (input.waitForCompletion === false || created.type === "certscore_scan") {
|
|
78
|
-
|
|
314
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
|
|
315
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
79
316
|
}
|
|
317
|
+
const waitBudget = resolveMcpScanSiteWaitBudget({
|
|
318
|
+
maxWaitSeconds: input.maxWaitSeconds,
|
|
319
|
+
nowMs: Date.now(),
|
|
320
|
+
startedAtMs: toolStartedAtMs,
|
|
321
|
+
});
|
|
322
|
+
if (waitBudget.remainingWaitMs === 0) {
|
|
323
|
+
console.warn(JSON.stringify({
|
|
324
|
+
event: "mcp.certscore_scan_site.wait_budget_consumed",
|
|
325
|
+
jobId: created.jobId ?? null,
|
|
326
|
+
scanId: created.scanId ?? created.scan_id ?? null,
|
|
327
|
+
status: created.status ?? null,
|
|
328
|
+
...waitBudget,
|
|
329
|
+
}));
|
|
330
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
|
|
331
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
332
|
+
}
|
|
333
|
+
const waitAbortController = new AbortController();
|
|
334
|
+
const waitAbortTimer = setTimeout(() => waitAbortController.abort(), waitBudget.remainingWaitMs);
|
|
80
335
|
try {
|
|
336
|
+
const internalMcpOperation = { operation: "scan_site_wait", scanId: created.scanId ?? created.scan_id ?? created.jobId };
|
|
81
337
|
const completed = await client.scans.wait(created, {
|
|
82
|
-
maxWaitMs:
|
|
338
|
+
maxWaitMs: waitBudget.remainingWaitMs,
|
|
339
|
+
internalMcpOperation,
|
|
340
|
+
signal: waitAbortController.signal,
|
|
83
341
|
});
|
|
84
|
-
|
|
342
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance({
|
|
85
343
|
...completed,
|
|
86
344
|
...scanCreationMetadata(created)
|
|
87
|
-
}));
|
|
345
|
+
}), demoSubstitution);
|
|
346
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
88
347
|
}
|
|
89
348
|
catch (error) {
|
|
90
349
|
const scanId = created.scanId ?? created.scan_id;
|
|
@@ -93,25 +352,18 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
93
352
|
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
94
353
|
jobId: created.jobId ?? null,
|
|
95
354
|
scanId: scanId ?? null,
|
|
96
|
-
status: created.status ?? null
|
|
355
|
+
status: created.status ?? null,
|
|
356
|
+
...waitBudget,
|
|
97
357
|
}));
|
|
98
|
-
if (error instanceof CertScoreTimeoutError && scanId) {
|
|
99
|
-
try {
|
|
100
|
-
return toToolResult(withMcpAgentGuidance({
|
|
101
|
-
...(await client.scans.status(scanId)),
|
|
102
|
-
...scanCreationMetadata(created)
|
|
103
|
-
}));
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// Creation already succeeded. Preserve that stable identity when
|
|
107
|
-
// the follow-up status read is briefly unavailable.
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
358
|
// Waiting is a convenience layered on top of scan creation. Once the
|
|
111
359
|
// API has accepted a scan, never turn a transient polling or hydration
|
|
112
360
|
// failure into an identity-less tool error that encourages callers to
|
|
113
361
|
// submit a second, non-idempotent certscore_scan_site request.
|
|
114
|
-
|
|
362
|
+
const guided = withExampleDomainDemo(withMcpAgentGuidance(created), demoSubstitution);
|
|
363
|
+
return toToolResult(guided, exampleDomainDemoText(guided, demoSubstitution));
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
clearTimeout(waitAbortTimer);
|
|
115
367
|
}
|
|
116
368
|
}
|
|
117
369
|
catch (error) {
|
|
@@ -126,41 +378,17 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
126
378
|
return toToolError(error);
|
|
127
379
|
}
|
|
128
380
|
});
|
|
129
|
-
registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }) => {
|
|
381
|
+
registerTool("certscore_get_scan_status", toolContract("certscore_get_scan_status"), async ({ scanId }, extra) => {
|
|
382
|
+
const client = clientForRequest(extra);
|
|
130
383
|
try {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
status: scan.status,
|
|
140
|
-
domain: scan.domain,
|
|
141
|
-
url: scan.url ?? null,
|
|
142
|
-
resultDisposition: scan.resultDisposition,
|
|
143
|
-
noGo: scan.noGo,
|
|
144
|
-
createdAt: scan.createdAt ?? null,
|
|
145
|
-
startedAt: scan.startedAt,
|
|
146
|
-
completedAt: scan.completedAt,
|
|
147
|
-
scanTimeSeconds: scan.scanTimeSeconds,
|
|
148
|
-
score: scan.score ?? null,
|
|
149
|
-
scoreStatus: scan.scoreStatus,
|
|
150
|
-
scoreVersion: scan.scoreVersion ?? null,
|
|
151
|
-
scoreUpdatedAt: scan.scoreUpdatedAt ?? null,
|
|
152
|
-
riskLevel: scan.riskLevel ?? null,
|
|
153
|
-
coverage: scan.coverage ?? null,
|
|
154
|
-
reportUrl: scan.links?.report ?? status.reportUrl ?? null,
|
|
155
|
-
links: { ...status.links, ...scan.links }
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Preserve the API status response if the terminal scan resource is
|
|
160
|
-
// briefly unavailable during eventual-consistency windows.
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return toToolResult(withMcpAgentGuidance(status));
|
|
384
|
+
const internalMcpOperation = { operation: "scan_status", scanId };
|
|
385
|
+
const status = await client.scans.status(scanId, { internalMcpOperation });
|
|
386
|
+
const guided = withMcpScanProvenanceGuidance({
|
|
387
|
+
...status,
|
|
388
|
+
jobId: undefined,
|
|
389
|
+
scanFrom: status.scanFrom ?? null
|
|
390
|
+
}, "existing_scan_retrieved");
|
|
391
|
+
return toToolResult(guided, scanStatusText(guided));
|
|
164
392
|
}
|
|
165
393
|
catch (error) {
|
|
166
394
|
return toToolError(error);
|
|
@@ -178,7 +406,16 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
178
406
|
detail: normalizeDetail(detail),
|
|
179
407
|
format: "json"
|
|
180
408
|
});
|
|
181
|
-
|
|
409
|
+
if (typeof result === "string") {
|
|
410
|
+
const guided = withMcpAgentGuidance({
|
|
411
|
+
type: "certscore_pulse_markdown",
|
|
412
|
+
scanId,
|
|
413
|
+
value: result
|
|
414
|
+
}, "existing_scan_retrieved");
|
|
415
|
+
return toToolResult(guided, markdownReportText(guided));
|
|
416
|
+
}
|
|
417
|
+
const guided = withMcpAgentGuidance(result, "existing_scan_retrieved");
|
|
418
|
+
return toToolResult(guided, pulseReportText(guided));
|
|
182
419
|
}
|
|
183
420
|
catch (error) {
|
|
184
421
|
return toToolError(error);
|
|
@@ -186,49 +423,65 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
186
423
|
});
|
|
187
424
|
registerTool("certscore_get_evidence", toolContract("certscore_get_evidence"), async ({ scanId }) => {
|
|
188
425
|
try {
|
|
189
|
-
|
|
426
|
+
const bounded = boundEvidencePacket(await client.getScan(scanId, { detail: "evidence", format: "json" }), MAX_EVIDENCE_PACKET_CHARS - 2_500);
|
|
427
|
+
const guided = withMcpAgentGuidance(bounded, "existing_scan_retrieved");
|
|
428
|
+
return toToolResult(guided, pulseReportText(guided, "CertScore evidence result"));
|
|
190
429
|
}
|
|
191
430
|
catch (error) {
|
|
192
431
|
return toToolError(error);
|
|
193
432
|
}
|
|
194
433
|
});
|
|
195
|
-
registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }) => {
|
|
434
|
+
registerTool("certscore_get_scan_bundle", toolContract("certscore_get_scan_bundle"), async ({ scanId, detail = "summary", maxBytes, maxFindings, maxPreConsentRows }, extra) => {
|
|
435
|
+
const client = clientForRequest(extra);
|
|
196
436
|
try {
|
|
197
|
-
const
|
|
437
|
+
const responseCeilingBytes = options.toolProfile === "light"
|
|
438
|
+
? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
|
|
439
|
+
: 200_000;
|
|
440
|
+
const requestedMaxBytes = maxBytes ?? (options.toolProfile === "light"
|
|
441
|
+
? LIGHT_MCP_BUNDLE_RESPONSE_CEILING_BYTES
|
|
442
|
+
: 50_000);
|
|
443
|
+
const internalMcpOperation = { operation: "scan_bundle", scanId };
|
|
444
|
+
const scan = await retryTransientOriginFailure(() => client.scans.get(scanId, { internalMcpOperation }));
|
|
198
445
|
if (scan.status === "completed_limited" && scan.resultDisposition === "no_go") {
|
|
199
|
-
|
|
446
|
+
const bundle = buildScanBundle({
|
|
200
447
|
detail,
|
|
201
448
|
evidence: null,
|
|
202
449
|
findings: { type: "certscore_finding_list", scanId, findings: [] },
|
|
203
|
-
maxBytes,
|
|
450
|
+
maxBytes: requestedMaxBytes,
|
|
204
451
|
maxFindings,
|
|
205
452
|
maxPreConsentRows,
|
|
206
453
|
preConsentCookiesTrackers: null,
|
|
207
454
|
report: null,
|
|
455
|
+
requestedMaxBytes,
|
|
456
|
+
responseCeilingBytes,
|
|
208
457
|
scan
|
|
209
|
-
})
|
|
458
|
+
});
|
|
459
|
+
return toToolResult(bundle, scanBundleText(bundle));
|
|
210
460
|
}
|
|
211
461
|
const includeEvidence = detail === "evidence" || detail === "full";
|
|
212
462
|
const reportDetail = detail === "full" ? "full" : includeEvidence ? "evidence" : "summary";
|
|
213
463
|
const [report, findings, preConsentCookiesTrackers] = await Promise.all([
|
|
214
|
-
retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json" })),
|
|
215
|
-
retryTransientOriginFailure(() => client.findings.list(scanId)),
|
|
216
|
-
|
|
217
|
-
? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId))
|
|
464
|
+
retryTransientOriginFailure(() => client.getScan(scanId, { detail: reportDetail, format: "json", internalMcpOperation })),
|
|
465
|
+
retryTransientOriginFailure(() => client.findings.list(scanId, { internalMcpOperation })),
|
|
466
|
+
scan.status === "completed"
|
|
467
|
+
? retryTransientOriginFailure(() => client.scans.preConsentCookiesTrackers(scanId, { internalMcpOperation }))
|
|
218
468
|
: Promise.resolve(null)
|
|
219
469
|
]);
|
|
220
470
|
const evidence = includeEvidence ? report : null;
|
|
221
|
-
|
|
471
|
+
const bundle = buildScanBundle({
|
|
222
472
|
detail,
|
|
223
473
|
evidence,
|
|
224
474
|
findings,
|
|
225
|
-
maxBytes,
|
|
475
|
+
maxBytes: requestedMaxBytes,
|
|
226
476
|
maxFindings,
|
|
227
477
|
maxPreConsentRows,
|
|
228
478
|
preConsentCookiesTrackers,
|
|
229
479
|
report,
|
|
480
|
+
requestedMaxBytes,
|
|
481
|
+
responseCeilingBytes,
|
|
230
482
|
scan
|
|
231
|
-
})
|
|
483
|
+
});
|
|
484
|
+
return toToolResult(bundle, scanBundleText(bundle));
|
|
232
485
|
}
|
|
233
486
|
catch (error) {
|
|
234
487
|
return toToolError(error);
|
|
@@ -237,7 +490,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
237
490
|
registerTool("certscore_export_findings", toolContract("certscore_export_findings"), async ({ scanId }) => {
|
|
238
491
|
try {
|
|
239
492
|
const report = await client.getScan(scanId, { detail: "full", format: "json" });
|
|
240
|
-
|
|
493
|
+
const guided = withMcpAgentGuidance(exportFindings(report), "existing_scan_retrieved");
|
|
494
|
+
return toToolResult(guided, findingListText(guided, "Exported canonical projected findings"));
|
|
241
495
|
}
|
|
242
496
|
catch (error) {
|
|
243
497
|
return toToolError(error);
|
|
@@ -245,7 +499,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
245
499
|
});
|
|
246
500
|
registerTool("certscore_list_findings", toolContract("certscore_list_findings"), async ({ limit, offset, scanId }) => {
|
|
247
501
|
try {
|
|
248
|
-
|
|
502
|
+
const guided = withMcpAgentGuidance(paginateFindingList(await client.findings.list(scanId), { limit, offset }), "existing_scan_retrieved");
|
|
503
|
+
return toToolResult(guided, findingListText(guided));
|
|
249
504
|
}
|
|
250
505
|
catch (error) {
|
|
251
506
|
return toToolError(error);
|
|
@@ -253,7 +508,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
253
508
|
});
|
|
254
509
|
registerTool("certscore_get_pre_consent_cookies_trackers", toolContract("certscore_get_pre_consent_cookies_trackers"), async ({ maxRows, scanId }) => {
|
|
255
510
|
try {
|
|
256
|
-
|
|
511
|
+
const guided = withMcpAgentGuidance(limitPreConsentRows(await client.scans.preConsentCookiesTrackers(scanId), { maxRows }), "existing_scan_retrieved");
|
|
512
|
+
return toToolResult(guided, preConsentInventoryText(guided));
|
|
257
513
|
}
|
|
258
514
|
catch (error) {
|
|
259
515
|
return toToolError(error);
|
|
@@ -277,7 +533,8 @@ export function createCertScoreMcpServer(options = {}) {
|
|
|
277
533
|
});
|
|
278
534
|
registerTool("certscore_get_latest_domain_pre_consent_cookies_trackers", toolContract("certscore_get_latest_domain_pre_consent_cookies_trackers"), async ({ domain, maxRows, scanFrom }) => {
|
|
279
535
|
try {
|
|
280
|
-
|
|
536
|
+
const guided = withMcpAgentGuidance(limitPreConsentRows(await client.domains.latestPreConsentCookiesTrackers(domain, { scanFrom }), { maxRows }), "existing_scan_retrieved");
|
|
537
|
+
return toToolResult(guided, preConsentInventoryText(guided));
|
|
281
538
|
}
|
|
282
539
|
catch (error) {
|
|
283
540
|
return toToolError(error);
|