kritya 0.8.11-beta → 0.8.12-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/client.js +23 -0
- package/dist/trust/mcpTrust.js +37 -0
- package/package.json +1 -1
package/dist/mcp/client.js
CHANGED
|
@@ -7,6 +7,13 @@ import { McpAuthRequiredError } from "./oauth.js";
|
|
|
7
7
|
import { missingVars } from "./servers.js";
|
|
8
8
|
import { HttpTransport, StdioTransport } from "./transport.js";
|
|
9
9
|
import { isPrivateOrLoopbackHost } from "../net/urlSafety.js";
|
|
10
|
+
import { checkToolsShape, serverFingerprint } from "../trust/mcpTrust.js";
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when a server's live tool shape no longer matches what was recorded
|
|
13
|
+
* the first time its config was trusted — see checkToolsShape.
|
|
14
|
+
*/
|
|
15
|
+
class McpShapeChangedError extends Error {
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
18
|
* Model Context Protocol client with two transports and no SDK dependency
|
|
12
19
|
* (to keep the install lean):
|
|
@@ -716,6 +723,9 @@ export async function connectServer(name, cfg, trace) {
|
|
|
716
723
|
});
|
|
717
724
|
const listed = await conn.initialize();
|
|
718
725
|
const specs = listed.tools;
|
|
726
|
+
const shape = checkToolsShape(serverFingerprint(cfg), specs, trace?.mcpTrustFile);
|
|
727
|
+
if (!shape.ok)
|
|
728
|
+
throw new McpShapeChangedError(name);
|
|
719
729
|
connections.push(conn);
|
|
720
730
|
// Reconnects re-derive names from scratch; releasing the old claims first
|
|
721
731
|
// keeps a server from colliding with its own previous incarnation.
|
|
@@ -747,6 +757,19 @@ export async function connectServer(name, cfg, trace) {
|
|
|
747
757
|
span.setAttribute("kritya.mcp_needs_auth", true);
|
|
748
758
|
span.setStatus("OK");
|
|
749
759
|
}
|
|
760
|
+
else if (err instanceof McpShapeChangedError) {
|
|
761
|
+
status.shapeChanged = true;
|
|
762
|
+
status.error =
|
|
763
|
+
`its tools no longer match what you approved — run "/mcp trust revoke ${name}" ` +
|
|
764
|
+
`and restart kritya to review and re-approve it`;
|
|
765
|
+
span.setAttribute("kritya.mcp_shape_changed", true);
|
|
766
|
+
span.setStatus("ERROR", "tool shape changed");
|
|
767
|
+
trace?.audit?.logTool({
|
|
768
|
+
tool: "mcp_connect",
|
|
769
|
+
summary: `server "${name}" tool shape changed since approval`,
|
|
770
|
+
outcome: "error",
|
|
771
|
+
});
|
|
772
|
+
}
|
|
750
773
|
else {
|
|
751
774
|
status.error = err instanceof Error ? err.message : String(err);
|
|
752
775
|
process.stderr.write(`kritya: MCP server "${name}" failed to start: ${status.error}\n`);
|
package/dist/trust/mcpTrust.js
CHANGED
|
@@ -41,6 +41,17 @@ export function serverFingerprint(cfg) {
|
|
|
41
41
|
};
|
|
42
42
|
return crypto.createHash("sha256").update(JSON.stringify(shape)).digest("hex");
|
|
43
43
|
}
|
|
44
|
+
/** Stable identity for a server's live tool list — what it actually does, not where it runs. */
|
|
45
|
+
export function toolsShapeHash(tools) {
|
|
46
|
+
const shape = tools
|
|
47
|
+
.map((t) => ({
|
|
48
|
+
name: t.name,
|
|
49
|
+
description: t.description ?? "",
|
|
50
|
+
inputSchema: t.inputSchema ?? null,
|
|
51
|
+
}))
|
|
52
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
+
return crypto.createHash("sha256").update(JSON.stringify(shape)).digest("hex");
|
|
54
|
+
}
|
|
44
55
|
function loadStore(storeFile) {
|
|
45
56
|
try {
|
|
46
57
|
const parsed = JSON.parse(fs.readFileSync(storeFile, "utf8"));
|
|
@@ -109,3 +120,29 @@ export function partitionByTrust(servers, storeFile = MCP_TRUST_FILE) {
|
|
|
109
120
|
}
|
|
110
121
|
return { trusted, pending };
|
|
111
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Approving a server's *config* (command, url, env keys, ...) only promises
|
|
125
|
+
* the user reviewed where it runs and what it can reach — not what tools it
|
|
126
|
+
* actually exposes to the model, which a compromised or malicious server can
|
|
127
|
+
* change on any connection without touching its declared config at all. This
|
|
128
|
+
* compares a freshly-connected server's live tool shape (names, descriptions,
|
|
129
|
+
* input schemas) against what was recorded the first time its config was
|
|
130
|
+
* trusted.
|
|
131
|
+
*
|
|
132
|
+
* There is nothing to compare on that first connection (or after upgrading
|
|
133
|
+
* from a version that didn't record this), so it's recorded rather than
|
|
134
|
+
* flagged — otherwise every existing approval would start failing the moment
|
|
135
|
+
* this check shipped.
|
|
136
|
+
*/
|
|
137
|
+
export function checkToolsShape(fingerprint, tools, storeFile = MCP_TRUST_FILE) {
|
|
138
|
+
const entries = loadStore(storeFile);
|
|
139
|
+
const entry = entries.find((e) => e.fingerprint === fingerprint);
|
|
140
|
+
if (!entry)
|
|
141
|
+
return { ok: true, recorded: false };
|
|
142
|
+
const hash = toolsShapeHash(tools);
|
|
143
|
+
if (!entry.toolsHash) {
|
|
144
|
+
saveStore(storeFile, entries.map((e) => (e === entry ? { ...e, toolsHash: hash } : e)));
|
|
145
|
+
return { ok: true, recorded: true };
|
|
146
|
+
}
|
|
147
|
+
return { ok: entry.toolsHash === hash, recorded: false };
|
|
148
|
+
}
|
package/package.json
CHANGED