@nebulacomponents/citable 1.15.0 → 1.16.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/CHANGELOG.md +32 -0
- package/README.md +3 -3
- package/dist/universal/.agents/skills/citable/SKILL.md +5 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
- package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.agents/skills/citable/manifest.json +14 -11
- package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +5 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
- package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.claude/skills/citable/manifest.json +14 -11
- package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/SKILL.md +5 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
- package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +14 -11
- package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/SKILL.md +5 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
- package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +14 -11
- package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/SKILL.md +5 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +3 -1
- package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.github/skills/citable/manifest.json +14 -11
- package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/SKILL.md +5 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
- package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +14 -11
- package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/SKILL.md +5 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
- package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +14 -11
- package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +5 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
- package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +14 -11
- package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/SKILL.md +5 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
- package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +14 -11
- package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/SKILL.md +5 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
- package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +14 -11
- package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/SKILL.md +5 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae/skills/citable/manifest.json +14 -11
- package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +5 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +14 -11
- package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/manifest.json +172 -136
- package/package.json +1 -1
- package/release/surfaces.json +2 -2
- package/schemas/experiment.schema.json +1 -1
- package/schemas/fleet.schema.json +93 -0
- package/schemas/funnel.schema.json +116 -0
- package/schemas/mcp-transport-envelope.schema.json +244 -0
- package/schemas/observation.schema.json +1 -1
- package/skill/SKILL.md +5 -1
- package/skill/commands/README.md +3 -1
- package/skill/commands/governance.md +12 -0
- package/skill/commands/measurement.md +20 -0
- package/skill/commands/reporting.md +16 -0
- package/src/cli/index.js +383 -8
- package/src/commands/attributeImpact.js +54 -0
- package/src/commands/connect.js +3 -0
- package/src/commands/edgeRules.js +120 -0
- package/src/commands/exceptions.js +241 -0
- package/src/commands/fleet.js +104 -0
- package/src/commands/generateLlmsTxt.js +111 -0
- package/src/commands/inspectAeo.js +74 -0
- package/src/commands/inspectCro.js +82 -0
- package/src/commands/inspectGeo.js +52 -0
- package/src/commands/inspectSerp.js +317 -0
- package/src/commands/lintComponents.js +132 -0
- package/src/commands/monitor.js +138 -0
- package/src/commands/observe.js +6 -1
- package/src/commands/planExperiment.js +68 -0
- package/src/commands/probe.js +70 -0
- package/src/commands/reportConsensus.js +48 -0
- package/src/commands/testFunnel.js +107 -0
- package/src/connectors/index.js +6 -1
- package/src/connectors/indexnow.js +431 -0
- package/src/connectors/mcp/allowlist.js +107 -0
- package/src/connectors/mcp/client.js +239 -0
- package/src/connectors/mcp/diagnostics.js +214 -0
- package/src/connectors/mcp/gscServer.js +155 -0
- package/src/connectors/mcp/http.js +130 -0
- package/src/connectors/mcp/index.js +7 -0
- package/src/connectors/mcp/pilot.js +138 -0
- package/src/connectors/mcp/pilotServer.js +80 -0
- package/src/connectors/mcp/sanitizer.js +38 -0
- package/src/connectors/mcp/stdio.js +178 -0
- package/src/crawler/ipRanges.js +161 -0
- package/src/detectors/agent.js +127 -0
- package/src/detectors/ans.js +282 -0
- package/src/detectors/crawl.js +46 -0
- package/src/detectors/cro.js +777 -0
- package/src/detectors/cwv.js +38 -1
- package/src/detectors/evd.js +101 -0
- package/src/detectors/framework.js +1 -1
- package/src/detectors/geoReco.js +408 -0
- package/src/detectors/index.js +4 -2
- package/src/detectors/link.js +112 -1
- package/src/detectors/page.js +101 -0
- package/src/detectors/schemaData.js +529 -0
- package/src/detectors/tech.js +306 -0
- package/src/extractor/page.js +186 -2
- package/src/extractor/site.js +9 -2
- package/src/installer/index.js +45 -2
- package/src/monitoring/alertDelivery.js +101 -7
- package/src/observations/attribution.js +217 -0
- package/src/registries/index.js +14 -0
- package/src/reporting/consensus.js +416 -0
- package/src/reporting/executiveExport.js +132 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport adapter for Model Context Protocol (MCP).
|
|
3
|
+
*
|
|
4
|
+
* Security Invariants:
|
|
5
|
+
* - Refuses private, loopback, or non-public network destinations unless explicitly bypassed for local test harnesses.
|
|
6
|
+
* - Requires HTTPS in production.
|
|
7
|
+
* - Enforces bounded request timeouts and response body limits.
|
|
8
|
+
* - Strips credentials from URLs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { validatePublicUrl } from "../../crawler/fetch.js";
|
|
12
|
+
|
|
13
|
+
export class McpHttpTransport {
|
|
14
|
+
constructor({
|
|
15
|
+
endpoint,
|
|
16
|
+
headers = {},
|
|
17
|
+
timeoutMs = 10000,
|
|
18
|
+
maxBodyBytes = 1048576,
|
|
19
|
+
allowLocalTest = false,
|
|
20
|
+
fetchImpl = globalThis.fetch,
|
|
21
|
+
}) {
|
|
22
|
+
if (!endpoint) throw new Error("McpHttpTransport requires endpoint URL");
|
|
23
|
+
this.endpoint = endpoint;
|
|
24
|
+
this.headers = headers;
|
|
25
|
+
this.timeoutMs = Math.min(Math.max(Number(timeoutMs) || 10000, 100), 60000);
|
|
26
|
+
this.maxBodyBytes = Number(maxBodyBytes) || 1048576;
|
|
27
|
+
this.allowLocalTest = Boolean(allowLocalTest);
|
|
28
|
+
this.fetchImpl = fetchImpl;
|
|
29
|
+
this.nextId = 1;
|
|
30
|
+
this.closed = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async validateEndpoint() {
|
|
34
|
+
if (this.allowLocalTest) {
|
|
35
|
+
const parsed = new URL(this.endpoint);
|
|
36
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
37
|
+
throw new Error(`unsupported URL protocol: ${parsed.protocol}`);
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parsed = await validatePublicUrl(this.endpoint);
|
|
43
|
+
if (parsed.protocol !== "https:") {
|
|
44
|
+
throw new Error(`remote MCP HTTP endpoint must use HTTPS: ${this.endpoint}`);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async sendRequest(method, params = {}) {
|
|
50
|
+
if (this.closed) throw new Error("McpHttpTransport is closed");
|
|
51
|
+
await this.validateEndpoint();
|
|
52
|
+
|
|
53
|
+
const id = this.nextId++;
|
|
54
|
+
const payload = JSON.stringify({
|
|
55
|
+
jsonrpc: "2.0",
|
|
56
|
+
id,
|
|
57
|
+
method,
|
|
58
|
+
params,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(new Error(`McpHttpTransport timeout after ${this.timeoutMs}ms`)), this.timeoutMs);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const res = await (this.fetchImpl || fetch)(this.endpoint, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
"accept": "application/json",
|
|
70
|
+
...this.headers,
|
|
71
|
+
},
|
|
72
|
+
body: payload,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(`MCP HTTP transport error: server returned HTTP ${res.status} ${res.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check Content-Length if present
|
|
81
|
+
const declared = Number(res.headers.get("content-length"));
|
|
82
|
+
if (Number.isFinite(declared) && declared > this.maxBodyBytes) {
|
|
83
|
+
throw new Error(`MCP HTTP response exceeds maximum body limit of ${this.maxBodyBytes} bytes`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
if (Buffer.byteLength(text, "utf8") > this.maxBodyBytes) {
|
|
88
|
+
throw new Error(`MCP HTTP response exceeds maximum body limit of ${this.maxBodyBytes} bytes`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(text);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(`MCP HTTP response is not valid JSON-RPC: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (parsed.error) {
|
|
99
|
+
throw new Error(`MCP RPC error ${parsed.error.code}: ${parsed.error.message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { result: parsed.result, raw: text };
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async sendNotification(method, params = {}) {
|
|
109
|
+
if (this.closed) return;
|
|
110
|
+
try {
|
|
111
|
+
await this.validateEndpoint();
|
|
112
|
+
const payload = JSON.stringify({
|
|
113
|
+
jsonrpc: "2.0",
|
|
114
|
+
method,
|
|
115
|
+
params,
|
|
116
|
+
});
|
|
117
|
+
await (this.fetchImpl || fetch)(this.endpoint, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "content-type": "application/json", ...this.headers },
|
|
120
|
+
body: payload,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
// Notifications do not wait for or guarantee delivery
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async close() {
|
|
128
|
+
this.closed = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { McpClient } from "./client.js";
|
|
2
|
+
export { McpStdioTransport } from "./stdio.js";
|
|
3
|
+
export { McpHttpTransport } from "./http.js";
|
|
4
|
+
export { ALLOWED_MCP_SERVERS, isServerAllowlisted, isToolAllowlisted, assertToolAllowed } from "./allowlist.js";
|
|
5
|
+
export { sanitizeSecrets } from "./sanitizer.js";
|
|
6
|
+
export { collectMcpEvidence, mcpConnector } from "./pilot.js";
|
|
7
|
+
export { diagnoseMcpTransports } from "./diagnostics.js";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pilot connector and evidence collector using Model Context Protocol (MCP).
|
|
3
|
+
*
|
|
4
|
+
* Security Invariants:
|
|
5
|
+
* - Read-only pilot operations only.
|
|
6
|
+
* - Transport outputs are saved as immutable observation packages.
|
|
7
|
+
* - Disclaimers explicitly record that MCP transport does not establish evidence authority.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { observationRun } from "../../observations/common.js";
|
|
13
|
+
import { McpClient } from "./client.js";
|
|
14
|
+
import { McpStdioTransport } from "./stdio.js";
|
|
15
|
+
import { McpHttpTransport } from "./http.js";
|
|
16
|
+
|
|
17
|
+
const THIS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const DEFAULT_PILOT_SERVER_SCRIPT = path.join(THIS_DIR, "pilotServer.js");
|
|
19
|
+
const DEFAULT_GSC_SERVER_SCRIPT = path.join(THIS_DIR, "gscServer.js");
|
|
20
|
+
|
|
21
|
+
export async function collectMcpEvidence(root, {
|
|
22
|
+
serverId = "citable-evidence-pilot",
|
|
23
|
+
toolName = "inspect_target",
|
|
24
|
+
args = {},
|
|
25
|
+
transportType = "stdio",
|
|
26
|
+
command = process.execPath,
|
|
27
|
+
serverScript = null,
|
|
28
|
+
endpoint = null,
|
|
29
|
+
timeoutMs = 10000,
|
|
30
|
+
maxBytes = 1048576,
|
|
31
|
+
allowLocalTest = false,
|
|
32
|
+
fetchImpl = globalThis.fetch,
|
|
33
|
+
} = {}) {
|
|
34
|
+
let transport;
|
|
35
|
+
let transportTarget;
|
|
36
|
+
|
|
37
|
+
const resolvedScript = serverScript || (serverId === "gsc-mcp" ? DEFAULT_GSC_SERVER_SCRIPT : DEFAULT_PILOT_SERVER_SCRIPT);
|
|
38
|
+
|
|
39
|
+
if (transportType === "stdio") {
|
|
40
|
+
transportTarget = `${command} ${resolvedScript}`;
|
|
41
|
+
transport = new McpStdioTransport({
|
|
42
|
+
command,
|
|
43
|
+
args: [resolvedScript],
|
|
44
|
+
timeoutMs,
|
|
45
|
+
maxBufferBytes: maxBytes,
|
|
46
|
+
});
|
|
47
|
+
} else if (transportType === "http") {
|
|
48
|
+
transportTarget = endpoint;
|
|
49
|
+
transport = new McpHttpTransport({
|
|
50
|
+
endpoint,
|
|
51
|
+
timeoutMs,
|
|
52
|
+
maxBodyBytes: maxBytes,
|
|
53
|
+
allowLocalTest,
|
|
54
|
+
fetchImpl,
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`unsupported MCP transport type: ${transportType}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const client = new McpClient({
|
|
61
|
+
serverId,
|
|
62
|
+
transport,
|
|
63
|
+
transportType,
|
|
64
|
+
transportTarget,
|
|
65
|
+
timeoutMs,
|
|
66
|
+
maxBytes,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await client.initialize();
|
|
71
|
+
const { transportEnvelope, observation } = await client.callTool(toolName, args);
|
|
72
|
+
|
|
73
|
+
const runTarget = args.url || serverId;
|
|
74
|
+
const run = observationRun(root, "observe mcp", runTarget, [observation], {
|
|
75
|
+
artifacts: {
|
|
76
|
+
"mcp-transport-envelope.json": transportEnvelope,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
runId: run.runId,
|
|
82
|
+
dir: run.dir,
|
|
83
|
+
transportEnvelope,
|
|
84
|
+
observation,
|
|
85
|
+
manifest: run.manifest,
|
|
86
|
+
};
|
|
87
|
+
} finally {
|
|
88
|
+
await client.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const mcpConnector = {
|
|
93
|
+
provider: "mcp",
|
|
94
|
+
defaultCredentialEnv: "MCP_AUTH_TOKEN",
|
|
95
|
+
readOnlyScopes: ["mcp:read"],
|
|
96
|
+
describeMetrics() {
|
|
97
|
+
return {
|
|
98
|
+
indexedPages: { unit: "count", value_type: "integer" },
|
|
99
|
+
crawlCoverage: { unit: "percentage", value_type: "number" },
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
async discoverProperties(context = {}) {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
property_id: "citable-evidence-pilot",
|
|
106
|
+
display_name: "Citable Evidence Pilot MCP Server",
|
|
107
|
+
account: "citable-pilot",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
property_id: "gsc-mcp",
|
|
111
|
+
display_name: "Google Search Console MCP Adapter",
|
|
112
|
+
account: "gsc-read-only",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
},
|
|
116
|
+
async validateConnection(connection, context = {}) {
|
|
117
|
+
const valid = connection.property_id === "citable-evidence-pilot" || connection.property_id === "gsc-mcp" || connection.provider === "mcp";
|
|
118
|
+
return {
|
|
119
|
+
valid,
|
|
120
|
+
properties: await this.discoverProperties(context),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
async sync(connection, metrics, { startDate, endDate, ...context } = {}) {
|
|
124
|
+
return {
|
|
125
|
+
rows: metrics.map((metric) => ({
|
|
126
|
+
metric,
|
|
127
|
+
value: 1,
|
|
128
|
+
dimensions: { target: connection.property_id, date: startDate },
|
|
129
|
+
observed_at: `${startDate}T00:00:00.000Z`,
|
|
130
|
+
})),
|
|
131
|
+
cursor: endDate,
|
|
132
|
+
limitations: [
|
|
133
|
+
"Collected via pilot MCP evidence transport.",
|
|
134
|
+
"MCP transport does not confer independent third-party evidence authority or completeness.",
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Model Context Protocol (MCP) Stdio Pilot Server.
|
|
4
|
+
*
|
|
5
|
+
* Implements read-only evidence verification tools for testing and pilot runs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import readline from "node:readline";
|
|
9
|
+
|
|
10
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
11
|
+
|
|
12
|
+
rl.on("line", (line) => {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed) return;
|
|
15
|
+
try {
|
|
16
|
+
const msg = JSON.parse(trimmed);
|
|
17
|
+
if (!msg || msg.id === undefined) return;
|
|
18
|
+
|
|
19
|
+
if (msg.method === "initialize") {
|
|
20
|
+
const resp = {
|
|
21
|
+
jsonrpc: "2.0",
|
|
22
|
+
id: msg.id,
|
|
23
|
+
result: {
|
|
24
|
+
protocolVersion: "2024-11-05",
|
|
25
|
+
capabilities: { tools: {} },
|
|
26
|
+
serverInfo: { name: "citable-evidence-pilot", version: "1.0.0" },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
30
|
+
} else if (msg.method === "tools/list") {
|
|
31
|
+
const resp = {
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
id: msg.id,
|
|
34
|
+
result: {
|
|
35
|
+
tools: [
|
|
36
|
+
{
|
|
37
|
+
name: "inspect_target",
|
|
38
|
+
description: "Extract title, canonical URL, and meta tags for target URL",
|
|
39
|
+
inputSchema: { type: "object", required: ["url"], properties: { url: { type: "string" } } },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "get_canonical_evidence",
|
|
43
|
+
description: "Collect publisher canonical declarations and HTTP Link headers",
|
|
44
|
+
inputSchema: { type: "object", required: ["url"], properties: { url: { type: "string" } } },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "query_search_index",
|
|
48
|
+
description: "Query indexed URL status from provider search index",
|
|
49
|
+
inputSchema: { type: "object", required: ["url"], properties: { url: { type: "string" }, host: { type: "string" } } },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
55
|
+
} else if (msg.method === "tools/call") {
|
|
56
|
+
const name = msg.params?.name;
|
|
57
|
+
const args = msg.params?.arguments || {};
|
|
58
|
+
let content = [];
|
|
59
|
+
if (name === "inspect_target") {
|
|
60
|
+
content = [{ type: "text", text: JSON.stringify({ url: args.url, canonical: args.url, title: "Sample Title", robots: "index, follow" }) }];
|
|
61
|
+
} else if (name === "get_canonical_evidence") {
|
|
62
|
+
content = [{ type: "text", text: JSON.stringify({ url: args.url, canonical_tag: args.url, og_url: args.url, link_header: null }) }];
|
|
63
|
+
} else if (name === "query_search_index") {
|
|
64
|
+
content = [{ type: "text", text: JSON.stringify({ url: args.url, indexed: true, last_crawled: "2026-09-08T00:00:00Z", coverage_state: "Submitted and indexed" }) }];
|
|
65
|
+
} else {
|
|
66
|
+
const resp = { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Tool not found: ${name}` } };
|
|
67
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const resp = {
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
id: msg.id,
|
|
73
|
+
result: { content, isError: false },
|
|
74
|
+
};
|
|
75
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// Ignore malformed JSON-RPC frames
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizer for Model Context Protocol (MCP) transports.
|
|
3
|
+
*
|
|
4
|
+
* Security Invariants:
|
|
5
|
+
* - Credentials, bearer tokens, API keys, and secret parameters must never
|
|
6
|
+
* persist into raw or normalized observation packages.
|
|
7
|
+
* - Counts redactions accurately for envelope provenance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SENSITIVE_KEY_PATTERN = /^(authorization|bearer|token|secret|apiKey|api_key|password|credential|access_token|refresh_token)$/i;
|
|
11
|
+
const SENSITIVE_VALUE_PATTERN = /(?:bearer\s+[a-zA-Z0-9_\-.]{10,}|(?:ghp|gho|glpat|ya29|sk-[a-zA-Z0-9]{20,}))/i;
|
|
12
|
+
|
|
13
|
+
export function sanitizeSecrets(value, countRef = { count: 0 }) {
|
|
14
|
+
if (value === null || value === undefined) return value;
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
if (SENSITIVE_VALUE_PATTERN.test(value)) {
|
|
17
|
+
countRef.count++;
|
|
18
|
+
return "[REDACTED_SECRET]";
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.map((item) => sanitizeSecrets(item, countRef));
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "object") {
|
|
26
|
+
const copy = {};
|
|
27
|
+
for (const [k, v] of Object.entries(value)) {
|
|
28
|
+
if (SENSITIVE_KEY_PATTERN.test(k)) {
|
|
29
|
+
copy[k] = "[REDACTED_SECRET]";
|
|
30
|
+
countRef.count++;
|
|
31
|
+
} else {
|
|
32
|
+
copy[k] = sanitizeSecrets(v, countRef);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return copy;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio transport adapter for Model Context Protocol (MCP).
|
|
3
|
+
*
|
|
4
|
+
* Security Invariants:
|
|
5
|
+
* - Bounded timeouts on every RPC call.
|
|
6
|
+
* - Bounded buffer/payload sizes.
|
|
7
|
+
* - Graceful process cleanup and SIGKILL fallback.
|
|
8
|
+
* - Fail closed on non-zero exit codes, premature termination, or invalid JSON framing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import readline from "node:readline";
|
|
13
|
+
|
|
14
|
+
export class McpStdioTransport {
|
|
15
|
+
constructor({ command, args = [], cwd, env = {}, timeoutMs = 10000, maxBufferBytes = 1048576 }) {
|
|
16
|
+
if (!command) throw new Error("McpStdioTransport requires command");
|
|
17
|
+
this.command = command;
|
|
18
|
+
this.args = args;
|
|
19
|
+
this.cwd = cwd;
|
|
20
|
+
this.env = { ...process.env, ...env };
|
|
21
|
+
this.timeoutMs = Math.min(Math.max(Number(timeoutMs) || 10000, 100), 60000);
|
|
22
|
+
this.maxBufferBytes = Number(maxBufferBytes) || 1048576;
|
|
23
|
+
|
|
24
|
+
this.process = null;
|
|
25
|
+
this.nextId = 1;
|
|
26
|
+
this.pending = new Map();
|
|
27
|
+
this.closed = false;
|
|
28
|
+
this.totalBytes = 0;
|
|
29
|
+
this.stderrOutput = "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async start() {
|
|
33
|
+
if (this.process) return;
|
|
34
|
+
this.process = spawn(this.command, this.args, {
|
|
35
|
+
cwd: this.cwd,
|
|
36
|
+
env: this.env,
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.process.on("error", (err) => {
|
|
41
|
+
this._rejectAll(new Error(`McpStdioTransport process error: ${err.message}`));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.process.on("exit", (code, signal) => {
|
|
45
|
+
if (!this.closed && code !== 0) {
|
|
46
|
+
this._rejectAll(new Error(`McpStdioTransport process exited prematurely with code ${code} (signal: ${signal})`));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.process.stderr.on("data", (chunk) => {
|
|
51
|
+
if (this.stderrOutput.length < 16384) {
|
|
52
|
+
this.stderrOutput += chunk.toString("utf8").slice(0, 16384 - this.stderrOutput.length);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
input: this.process.stdout,
|
|
58
|
+
terminal: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
rl.on("line", (line) => {
|
|
62
|
+
this.totalBytes += Buffer.byteLength(line, "utf8");
|
|
63
|
+
if (this.totalBytes > this.maxBufferBytes) {
|
|
64
|
+
this._rejectAll(new Error(`McpStdioTransport exceeded max buffer limit of ${this.maxBufferBytes} bytes`));
|
|
65
|
+
this.close();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this._handleLine(line);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_handleLine(line) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (!trimmed) return;
|
|
75
|
+
let msg;
|
|
76
|
+
try {
|
|
77
|
+
msg = JSON.parse(trimmed);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// Non-JSON line ignored or unparseable framing
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (msg && msg.id !== undefined) {
|
|
84
|
+
const handler = this.pending.get(msg.id);
|
|
85
|
+
if (handler) {
|
|
86
|
+
this.pending.delete(msg.id);
|
|
87
|
+
clearTimeout(handler.timer);
|
|
88
|
+
if (msg.error) {
|
|
89
|
+
handler.reject(new Error(`MCP RPC error ${msg.error.code}: ${msg.error.message}`));
|
|
90
|
+
} else {
|
|
91
|
+
handler.resolve({ result: msg.result, raw: trimmed });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_rejectAll(err) {
|
|
98
|
+
for (const [id, handler] of this.pending.entries()) {
|
|
99
|
+
clearTimeout(handler.timer);
|
|
100
|
+
handler.reject(err);
|
|
101
|
+
}
|
|
102
|
+
this.pending.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async sendRequest(method, params = {}) {
|
|
106
|
+
if (this.closed) throw new Error("McpStdioTransport is closed");
|
|
107
|
+
if (!this.process) await this.start();
|
|
108
|
+
|
|
109
|
+
const id = this.nextId++;
|
|
110
|
+
const payload = JSON.stringify({
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
id,
|
|
113
|
+
method,
|
|
114
|
+
params,
|
|
115
|
+
}) + "\n";
|
|
116
|
+
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
if (this.pending.has(id)) {
|
|
120
|
+
this.pending.delete(id);
|
|
121
|
+
reject(new Error(`McpStdioTransport timeout after ${this.timeoutMs}ms waiting for method: ${method}`));
|
|
122
|
+
}
|
|
123
|
+
}, this.timeoutMs);
|
|
124
|
+
|
|
125
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
126
|
+
|
|
127
|
+
this.process.stdin.write(payload, (err) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
this.pending.delete(id);
|
|
131
|
+
reject(new Error(`failed to write to MCP stdio stdin: ${err.message}`));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sendNotification(method, params = {}) {
|
|
138
|
+
if (this.closed || !this.process) return;
|
|
139
|
+
const payload = JSON.stringify({
|
|
140
|
+
jsonrpc: "2.0",
|
|
141
|
+
method,
|
|
142
|
+
params,
|
|
143
|
+
}) + "\n";
|
|
144
|
+
this.process.stdin.write(payload);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async close() {
|
|
148
|
+
this.closed = true;
|
|
149
|
+
this._rejectAll(new Error("McpStdioTransport closed by client"));
|
|
150
|
+
if (!this.process) return;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
this.process.stdin.end();
|
|
154
|
+
} catch {}
|
|
155
|
+
|
|
156
|
+
const proc = this.process;
|
|
157
|
+
this.process = null;
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const killTimer = setTimeout(() => {
|
|
161
|
+
try { proc.kill("SIGKILL"); } catch {}
|
|
162
|
+
resolve();
|
|
163
|
+
}, 500);
|
|
164
|
+
|
|
165
|
+
proc.once("exit", () => {
|
|
166
|
+
clearTimeout(killTimer);
|
|
167
|
+
resolve();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
proc.kill("SIGTERM");
|
|
172
|
+
} catch {
|
|
173
|
+
clearTimeout(killTimer);
|
|
174
|
+
resolve();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|