@nebulacomponents/citable 1.15.1 → 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 +25 -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/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,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Model Context Protocol (MCP) client.
|
|
3
|
+
*
|
|
4
|
+
* Security Invariants:
|
|
5
|
+
* - Refuses unallowlisted servers and state-changing tools (fail-closed).
|
|
6
|
+
* - Hashes canonical arguments and raw payloads using SHA-256.
|
|
7
|
+
* - Enforces zero secret persistence via recursive sanitization.
|
|
8
|
+
* - Emits schema-validated mcp-transport-envelope records.
|
|
9
|
+
* - Normalizes into immutable Citable observation envelopes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { sha256 } from "../../shared/io.js";
|
|
13
|
+
import { validateAgainst } from "../../shared/schemaValidator.js";
|
|
14
|
+
import { envelope } from "../../observations/common.js";
|
|
15
|
+
import { ALLOWED_MCP_SERVERS, assertToolAllowed, isServerAllowlisted } from "./allowlist.js";
|
|
16
|
+
import { sanitizeSecrets } from "./sanitizer.js";
|
|
17
|
+
|
|
18
|
+
function canonicalStringify(obj) {
|
|
19
|
+
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
|
|
20
|
+
if (Array.isArray(obj)) return "[" + obj.map(canonicalStringify).join(",") + "]";
|
|
21
|
+
const keys = Object.keys(obj).sort();
|
|
22
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalStringify(obj[k])).join(",") + "}";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class McpClient {
|
|
26
|
+
constructor({
|
|
27
|
+
serverId,
|
|
28
|
+
transport,
|
|
29
|
+
transportType = "stdio",
|
|
30
|
+
transportTarget = null,
|
|
31
|
+
packageName = null,
|
|
32
|
+
pinnedVersion = null,
|
|
33
|
+
authorizationMode = "none",
|
|
34
|
+
scopes = [],
|
|
35
|
+
timeoutMs = 10000,
|
|
36
|
+
maxBytes = 1048576,
|
|
37
|
+
adapterVersion = "1.0.0",
|
|
38
|
+
normalizationVersion = "1.0.0",
|
|
39
|
+
}) {
|
|
40
|
+
if (!isServerAllowlisted(serverId)) {
|
|
41
|
+
throw new Error(`untrusted MCP server: "${serverId}" is not in the allowlist`);
|
|
42
|
+
}
|
|
43
|
+
if (!transport) {
|
|
44
|
+
throw new Error("McpClient requires an active transport instance");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.serverId = serverId;
|
|
48
|
+
this.serverConfig = ALLOWED_MCP_SERVERS[serverId];
|
|
49
|
+
this.transport = transport;
|
|
50
|
+
this.transportType = transportType;
|
|
51
|
+
this.transportTarget = transportTarget || serverId;
|
|
52
|
+
this.packageName = packageName;
|
|
53
|
+
this.pinnedVersion = pinnedVersion;
|
|
54
|
+
this.authorizationMode = authorizationMode;
|
|
55
|
+
this.scopes = scopes;
|
|
56
|
+
this.timeoutMs = Math.min(Math.max(Number(timeoutMs) || 10000, 100), 60000);
|
|
57
|
+
this.maxBytes = Number(maxBytes) || 1048576;
|
|
58
|
+
this.adapterVersion = adapterVersion;
|
|
59
|
+
this.normalizationVersion = normalizationVersion;
|
|
60
|
+
|
|
61
|
+
this.protocolVersion = "2024-11-05";
|
|
62
|
+
this.serverCapabilities = null;
|
|
63
|
+
this.serverInfo = null;
|
|
64
|
+
this.initialized = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async initialize() {
|
|
68
|
+
if (this.initialized) return { capabilities: this.serverCapabilities, serverInfo: this.serverInfo };
|
|
69
|
+
|
|
70
|
+
const initRes = await this.transport.sendRequest("initialize", {
|
|
71
|
+
protocolVersion: this.protocolVersion,
|
|
72
|
+
capabilities: {},
|
|
73
|
+
clientInfo: {
|
|
74
|
+
name: "@nebulacomponents/citable",
|
|
75
|
+
version: "1.15.1",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = initRes.result || {};
|
|
80
|
+
this.protocolVersion = result.protocolVersion || this.protocolVersion;
|
|
81
|
+
this.serverCapabilities = result.capabilities || {};
|
|
82
|
+
this.serverInfo = result.serverInfo || {};
|
|
83
|
+
|
|
84
|
+
await this.transport.sendNotification("notifications/initialized", {});
|
|
85
|
+
this.initialized = true;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
protocolVersion: this.protocolVersion,
|
|
89
|
+
capabilities: this.serverCapabilities,
|
|
90
|
+
serverInfo: this.serverInfo,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async listTools() {
|
|
95
|
+
if (!this.initialized) await this.initialize();
|
|
96
|
+
const res = await this.transport.sendRequest("tools/list", {});
|
|
97
|
+
const tools = (res.result && res.result.tools) || [];
|
|
98
|
+
|
|
99
|
+
return tools.map((tool) => {
|
|
100
|
+
const serverTool = this.serverConfig.tools[tool.name];
|
|
101
|
+
const allowlisted = Boolean(serverTool);
|
|
102
|
+
const isReadOnly = Boolean(serverTool && serverTool.read_only);
|
|
103
|
+
return {
|
|
104
|
+
name: tool.name,
|
|
105
|
+
description: tool.description,
|
|
106
|
+
inputSchema: tool.inputSchema,
|
|
107
|
+
allowlisted,
|
|
108
|
+
read_only: isReadOnly,
|
|
109
|
+
supported: allowlisted && isReadOnly,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async callTool(toolName, args = {}) {
|
|
115
|
+
const toolDef = assertToolAllowed(this.serverId, toolName, { isReadOnly: true });
|
|
116
|
+
if (!this.initialized) await this.initialize();
|
|
117
|
+
|
|
118
|
+
const startTime = performance.now();
|
|
119
|
+
const canonicalArgs = canonicalStringify(args);
|
|
120
|
+
const argumentsHash = sha256(canonicalArgs);
|
|
121
|
+
const inputSchemaHash = sha256(canonicalStringify(toolDef.schema || {}));
|
|
122
|
+
|
|
123
|
+
const redactions = { count: 0 };
|
|
124
|
+
const sanitizedArgs = sanitizeSecrets(args, redactions);
|
|
125
|
+
|
|
126
|
+
let res;
|
|
127
|
+
let isError = false;
|
|
128
|
+
try {
|
|
129
|
+
res = await this.transport.sendRequest("tools/call", {
|
|
130
|
+
name: toolName,
|
|
131
|
+
arguments: args,
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
isError = true;
|
|
135
|
+
res = {
|
|
136
|
+
result: {
|
|
137
|
+
isError: true,
|
|
138
|
+
content: [{ type: "text", text: err.message }],
|
|
139
|
+
},
|
|
140
|
+
raw: JSON.stringify({ error: err.message }),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
145
|
+
const raw = res.raw || JSON.stringify(res.result || {});
|
|
146
|
+
const rawSha256 = sha256(raw);
|
|
147
|
+
const contentBytes = Buffer.byteLength(raw, "utf8");
|
|
148
|
+
|
|
149
|
+
if (res.result && res.result.isError) {
|
|
150
|
+
isError = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rawContent = (res.result && res.result.content) || [];
|
|
154
|
+
const sanitizedContent = sanitizeSecrets(rawContent, redactions);
|
|
155
|
+
|
|
156
|
+
const transportEnvelope = {
|
|
157
|
+
schema_version: 1,
|
|
158
|
+
transport: {
|
|
159
|
+
type: this.transportType,
|
|
160
|
+
protocol_version: this.protocolVersion,
|
|
161
|
+
timeout_ms: this.timeoutMs,
|
|
162
|
+
max_bytes: this.maxBytes,
|
|
163
|
+
retries: 0,
|
|
164
|
+
},
|
|
165
|
+
server: {
|
|
166
|
+
server_id: this.serverId,
|
|
167
|
+
package_name: this.packageName,
|
|
168
|
+
pinned_version: this.pinnedVersion,
|
|
169
|
+
transport_target: this.transportTarget,
|
|
170
|
+
allowlisted: true,
|
|
171
|
+
capabilities: this.serverCapabilities || {},
|
|
172
|
+
},
|
|
173
|
+
tool: {
|
|
174
|
+
name: toolName,
|
|
175
|
+
description: toolDef.description || null,
|
|
176
|
+
read_only: true,
|
|
177
|
+
input_schema_hash: inputSchemaHash,
|
|
178
|
+
input_schema: toolDef.schema || {},
|
|
179
|
+
},
|
|
180
|
+
request: {
|
|
181
|
+
arguments_hash: argumentsHash,
|
|
182
|
+
arguments: sanitizedArgs,
|
|
183
|
+
authorization_mode: this.authorizationMode,
|
|
184
|
+
scopes: this.scopes,
|
|
185
|
+
},
|
|
186
|
+
response: {
|
|
187
|
+
raw_sha256: rawSha256,
|
|
188
|
+
content_bytes: contentBytes,
|
|
189
|
+
is_error: isError,
|
|
190
|
+
retention_reason: redactions.count > 0 ? "redacted_sensitive" : "raw_preserved",
|
|
191
|
+
content: sanitizedContent,
|
|
192
|
+
},
|
|
193
|
+
provenance: {
|
|
194
|
+
adapter_version: this.adapterVersion,
|
|
195
|
+
normalization_version: this.normalizationVersion,
|
|
196
|
+
sanitized_redactions: redactions.count,
|
|
197
|
+
duration_ms: durationMs,
|
|
198
|
+
},
|
|
199
|
+
authority: {
|
|
200
|
+
source_authority: "synthetic",
|
|
201
|
+
collection_authority: "direct_api",
|
|
202
|
+
authenticity_status: "transport_authenticated",
|
|
203
|
+
representativeness: "single_observation",
|
|
204
|
+
},
|
|
205
|
+
status: isError ? "failed" : "success",
|
|
206
|
+
collected_at: new Date().toISOString(),
|
|
207
|
+
limitations: [
|
|
208
|
+
"Evidence collected via allowlisted read-only MCP transport adapter.",
|
|
209
|
+
"MCP transport does not confer independent third-party evidence authority or completeness.",
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const check = validateAgainst("mcp-transport-envelope.schema.json", transportEnvelope);
|
|
214
|
+
if (!check.valid) {
|
|
215
|
+
throw new Error(`MCP transport envelope violates contract: ${check.errors.join("; ")}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const observation = envelope("mcp_transport", transportEnvelope, {
|
|
219
|
+
method: "live_api",
|
|
220
|
+
source: `mcp:${this.serverId}/${toolName}`,
|
|
221
|
+
state: isError ? "failed" : "observed",
|
|
222
|
+
confidence: "confirmed",
|
|
223
|
+
limitations: transportEnvelope.limitations,
|
|
224
|
+
raw,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
transportEnvelope,
|
|
229
|
+
observation,
|
|
230
|
+
isError,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async close() {
|
|
235
|
+
if (this.transport && typeof this.transport.close === "function") {
|
|
236
|
+
await this.transport.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { McpStdioTransport } from './stdio.js';
|
|
5
|
+
import { McpHttpTransport } from './http.js';
|
|
6
|
+
import { ALLOWED_MCP_SERVERS } from './allowlist.js';
|
|
7
|
+
import { readYaml } from '../../shared/io.js';
|
|
8
|
+
|
|
9
|
+
const THIS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
export async function diagnoseMcpTransports(options = {}) {
|
|
12
|
+
const checks = [];
|
|
13
|
+
const packageRoot = options.packageRoot || path.resolve(THIS_DIR, '../../..');
|
|
14
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
15
|
+
|
|
16
|
+
// 1. Allowlist and tool schema validation
|
|
17
|
+
let serversValid = true;
|
|
18
|
+
let totalServers = 0;
|
|
19
|
+
let totalTools = 0;
|
|
20
|
+
const allowlistErrors = [];
|
|
21
|
+
|
|
22
|
+
for (const [serverId, serverDef] of Object.entries(ALLOWED_MCP_SERVERS)) {
|
|
23
|
+
totalServers++;
|
|
24
|
+
if (!serverDef.description || typeof serverDef.description !== 'string') {
|
|
25
|
+
serversValid = false;
|
|
26
|
+
allowlistErrors.push(`server "${serverId}" lacks valid description`);
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(serverDef.transports) || serverDef.transports.length === 0) {
|
|
29
|
+
serversValid = false;
|
|
30
|
+
allowlistErrors.push(`server "${serverId}" has no valid transports`);
|
|
31
|
+
}
|
|
32
|
+
if (!serverDef.tools || typeof serverDef.tools !== 'object') {
|
|
33
|
+
serversValid = false;
|
|
34
|
+
allowlistErrors.push(`server "${serverId}" has no tools defined`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const [toolName, toolDef] of Object.entries(serverDef.tools)) {
|
|
38
|
+
totalTools++;
|
|
39
|
+
if (toolDef.read_only !== true) {
|
|
40
|
+
serversValid = false;
|
|
41
|
+
allowlistErrors.push(`tool "${serverId}.${toolName}" must be declared read_only: true`);
|
|
42
|
+
}
|
|
43
|
+
if (!toolDef.description || typeof toolDef.description !== 'string') {
|
|
44
|
+
serversValid = false;
|
|
45
|
+
allowlistErrors.push(`tool "${serverId}.${toolName}" lacks valid description`);
|
|
46
|
+
}
|
|
47
|
+
if (!toolDef.schema || toolDef.schema.type !== 'object') {
|
|
48
|
+
serversValid = false;
|
|
49
|
+
allowlistErrors.push(`tool "${serverId}.${toolName}" schema must be type: object`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (serversValid && totalServers > 0) {
|
|
55
|
+
checks.push({
|
|
56
|
+
level: 'PASS',
|
|
57
|
+
message: `MCP allowlist valid: ${totalServers} servers, ${totalTools} read-only tools verified against schema contracts`,
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
checks.push({
|
|
61
|
+
level: 'FAIL',
|
|
62
|
+
message: `MCP allowlist validation failed: ${allowlistErrors.join('; ')}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Pilot script presence and stdio probe
|
|
67
|
+
const pilotScript = options.pilotScript || path.resolve(packageRoot, 'src/connectors/mcp/pilotServer.js');
|
|
68
|
+
let stdioReady = false;
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(pilotScript)) {
|
|
71
|
+
checks.push({
|
|
72
|
+
level: 'PASS',
|
|
73
|
+
message: `MCP stdio pilot script verified at ${pilotScript}`,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Test non-destructive initialization handshake probe
|
|
77
|
+
const transport = new McpStdioTransport({
|
|
78
|
+
command: process.execPath,
|
|
79
|
+
args: [pilotScript],
|
|
80
|
+
timeoutMs: 3000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await transport.start();
|
|
85
|
+
const initRes = await transport.sendRequest('initialize', {
|
|
86
|
+
protocolVersion: '2024-11-05',
|
|
87
|
+
capabilities: {},
|
|
88
|
+
clientInfo: {
|
|
89
|
+
name: '@nebulacomponents/citable-doctor',
|
|
90
|
+
version: '1.0.0',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (initRes?.result?.serverInfo?.name === 'citable-evidence-pilot') {
|
|
95
|
+
stdioReady = true;
|
|
96
|
+
checks.push({
|
|
97
|
+
level: 'PASS',
|
|
98
|
+
message: `MCP stdio pilot server handshake responsive (protocol: ${initRes.result.protocolVersion || '2024-11-05'}, server: citable-evidence-pilot)`,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
checks.push({
|
|
102
|
+
level: 'WARN',
|
|
103
|
+
message: `MCP stdio pilot server returned unexpected identity: ${JSON.stringify(initRes?.result?.serverInfo)}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
checks.push({
|
|
108
|
+
level: 'FAIL',
|
|
109
|
+
message: `MCP stdio pilot server handshake failed: ${err.message}`,
|
|
110
|
+
});
|
|
111
|
+
} finally {
|
|
112
|
+
await transport.close();
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
checks.push({
|
|
116
|
+
level: 'FAIL',
|
|
117
|
+
message: `MCP stdio pilot script missing at ${pilotScript}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. HTTP endpoint accessibility and security guards
|
|
122
|
+
let httpGuardsActive = false;
|
|
123
|
+
let loopbackBlocked = false;
|
|
124
|
+
let nonHttpsBlocked = false;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const loopbackTransport = new McpHttpTransport({
|
|
128
|
+
endpoint: 'http://127.0.0.1:9999',
|
|
129
|
+
allowLocalTest: false,
|
|
130
|
+
});
|
|
131
|
+
await loopbackTransport.validateEndpoint();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err.message.includes('loopback') || err.message.includes('private') || err.message.includes('non-public') || err.message.includes('HTTPS')) {
|
|
134
|
+
loopbackBlocked = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const plainHttpTransport = new McpHttpTransport({
|
|
140
|
+
endpoint: 'http://example.com/mcp',
|
|
141
|
+
allowLocalTest: false,
|
|
142
|
+
});
|
|
143
|
+
await plainHttpTransport.validateEndpoint();
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err.message.includes('HTTPS') || err.message.includes('protocol')) {
|
|
146
|
+
nonHttpsBlocked = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (loopbackBlocked && nonHttpsBlocked) {
|
|
151
|
+
httpGuardsActive = true;
|
|
152
|
+
checks.push({
|
|
153
|
+
level: 'PASS',
|
|
154
|
+
message: 'MCP HTTP endpoint security guards active (enforces HTTPS and blocks private/loopback destinations)',
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
checks.push({
|
|
158
|
+
level: 'FAIL',
|
|
159
|
+
message: `MCP HTTP security guards compromised (loopbackBlocked=${loopbackBlocked}, nonHttpsBlocked=${nonHttpsBlocked})`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 4. Registry bindings (.citable/connections.yaml)
|
|
164
|
+
let connectionsFound = 0;
|
|
165
|
+
const connPath = path.join(projectRoot, '.citable', 'connections.yaml');
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(connPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = readYaml(connPath);
|
|
170
|
+
const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
|
|
171
|
+
const mcpEntries = entries.filter((e) => e.provider === 'mcp' || Boolean(ALLOWED_MCP_SERVERS[e.provider]));
|
|
172
|
+
|
|
173
|
+
if (mcpEntries.length > 0) {
|
|
174
|
+
connectionsFound = mcpEntries.length;
|
|
175
|
+
for (const conn of mcpEntries) {
|
|
176
|
+
checks.push({
|
|
177
|
+
level: 'PASS',
|
|
178
|
+
message: `MCP connection ${conn.connection_id} bound in registry (provider: ${conn.provider}, state: ${conn.state})`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
checks.push({
|
|
183
|
+
level: 'PASS',
|
|
184
|
+
message: 'MCP connection registry present (.citable/connections.yaml); 0 active MCP bindings',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
checks.push({
|
|
189
|
+
level: 'WARN',
|
|
190
|
+
message: `Failed to read .citable/connections.yaml: ${err.message}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
checks.push({
|
|
195
|
+
level: 'WARN',
|
|
196
|
+
message: 'No .citable/connections.yaml registry found (project context uninitialized)',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const hasFail = checks.some((c) => c.level === 'FAIL');
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: !hasFail,
|
|
204
|
+
checks,
|
|
205
|
+
summary: {
|
|
206
|
+
stdio_ready: stdioReady,
|
|
207
|
+
http_guards_active: httpGuardsActive,
|
|
208
|
+
allowlist_valid: serversValid,
|
|
209
|
+
servers_count: totalServers,
|
|
210
|
+
tools_count: totalTools,
|
|
211
|
+
connections_found: connectionsFound,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Google Search Console (GSC) Read-Only MCP Stdio Adapter Server.
|
|
4
|
+
*
|
|
5
|
+
* Implements read-only search console inspection and query tools:
|
|
6
|
+
* - inspect_url: verify URL indexing status, crawl coverage, and canonical declarations
|
|
7
|
+
* - query_search_analytics: retrieve read-only query and page performance metrics
|
|
8
|
+
*
|
|
9
|
+
* Invariants:
|
|
10
|
+
* - Read-only operation only.
|
|
11
|
+
* - JSON-RPC 2.0 protocol over stdio.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import readline from "node:readline";
|
|
15
|
+
|
|
16
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
17
|
+
|
|
18
|
+
rl.on("line", (line) => {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed) return;
|
|
21
|
+
try {
|
|
22
|
+
const msg = JSON.parse(trimmed);
|
|
23
|
+
if (!msg || msg.id === undefined) return;
|
|
24
|
+
|
|
25
|
+
if (msg.method === "initialize") {
|
|
26
|
+
const resp = {
|
|
27
|
+
jsonrpc: "2.0",
|
|
28
|
+
id: msg.id,
|
|
29
|
+
result: {
|
|
30
|
+
protocolVersion: "2024-11-05",
|
|
31
|
+
capabilities: { tools: {} },
|
|
32
|
+
serverInfo: { name: "gsc-mcp", version: "1.0.0" },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
36
|
+
} else if (msg.method === "tools/list") {
|
|
37
|
+
const resp = {
|
|
38
|
+
jsonrpc: "2.0",
|
|
39
|
+
id: msg.id,
|
|
40
|
+
result: {
|
|
41
|
+
tools: [
|
|
42
|
+
{
|
|
43
|
+
name: "inspect_url",
|
|
44
|
+
description: "Inspect URL indexing status via Search Console",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
required: ["url", "siteUrl"],
|
|
48
|
+
properties: {
|
|
49
|
+
url: { type: "string" },
|
|
50
|
+
siteUrl: { type: "string" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "query_search_analytics",
|
|
56
|
+
description: "Query search analytics impressions and clicks",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
required: ["siteUrl", "startDate", "endDate"],
|
|
60
|
+
properties: {
|
|
61
|
+
siteUrl: { type: "string" },
|
|
62
|
+
startDate: { type: "string" },
|
|
63
|
+
endDate: { type: "string" },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
71
|
+
} else if (msg.method === "tools/call") {
|
|
72
|
+
const name = msg.params?.name;
|
|
73
|
+
const args = msg.params?.arguments || {};
|
|
74
|
+
let content = [];
|
|
75
|
+
|
|
76
|
+
if (name === "inspect_url") {
|
|
77
|
+
if (!args.url || !args.siteUrl) {
|
|
78
|
+
const resp = {
|
|
79
|
+
jsonrpc: "2.0",
|
|
80
|
+
id: msg.id,
|
|
81
|
+
error: { code: -32602, message: "Invalid params: url and siteUrl are required" },
|
|
82
|
+
};
|
|
83
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const inspectionResult = {
|
|
88
|
+
verdict: "PASS",
|
|
89
|
+
coverageState: "Submitted and indexed",
|
|
90
|
+
indexingState: "INDEXING_ALLOWED",
|
|
91
|
+
lastCrawlTime: "2026-09-08T00:00:00Z",
|
|
92
|
+
pageFetchState: "SUCCESSFUL",
|
|
93
|
+
robotsTxtState: "ALLOWED",
|
|
94
|
+
userCanonical: args.url,
|
|
95
|
+
googleCanonical: args.url,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
content = [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: JSON.stringify({
|
|
102
|
+
url: args.url,
|
|
103
|
+
siteUrl: args.siteUrl,
|
|
104
|
+
inspectionResult,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
} else if (name === "query_search_analytics") {
|
|
109
|
+
if (!args.siteUrl || !args.startDate || !args.endDate) {
|
|
110
|
+
const resp = {
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
id: msg.id,
|
|
113
|
+
error: { code: -32602, message: "Invalid params: siteUrl, startDate, and endDate are required" },
|
|
114
|
+
};
|
|
115
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
content = [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: JSON.stringify({
|
|
123
|
+
siteUrl: args.siteUrl,
|
|
124
|
+
startDate: args.startDate,
|
|
125
|
+
endDate: args.endDate,
|
|
126
|
+
rows: [
|
|
127
|
+
{
|
|
128
|
+
keys: [args.siteUrl],
|
|
129
|
+
clicks: 142,
|
|
130
|
+
impressions: 2840,
|
|
131
|
+
ctr: 0.05,
|
|
132
|
+
position: 4.2,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
responseAggregationType: "byPage",
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
} else {
|
|
140
|
+
const resp = { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Tool not found: ${name}` } };
|
|
141
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const resp = {
|
|
146
|
+
jsonrpc: "2.0",
|
|
147
|
+
id: msg.id,
|
|
148
|
+
result: { content, isError: false },
|
|
149
|
+
};
|
|
150
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
// Ignore malformed frames
|
|
154
|
+
}
|
|
155
|
+
});
|