@tryvira/mcp 0.1.12 → 0.1.13
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/LICENSE +21 -21
- package/README.md +79 -79
- package/dist/cli.js +2044 -2038
- package/dist/index.d.ts +306 -306
- package/dist/index.js +224 -224
- package/dist/migrations/001_vnext_foundation.sql +628 -628
- package/package.json +52 -52
package/dist/index.js
CHANGED
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
// ../runtime/src/mcp-gateway.ts
|
|
2
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
3
|
-
import { isIP } from "node:net";
|
|
4
|
-
|
|
5
|
-
// ../domain/src/scope-composer.ts
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
function canonical(value) {
|
|
8
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
9
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
10
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
11
|
-
}
|
|
12
|
-
var digest = (value) => createHash("sha256").update(canonical(value)).digest("hex");
|
|
13
|
-
function assertComposedScopeIntegrity(composed) {
|
|
14
|
-
const conflictSetDigest = digest(composed.conflicts);
|
|
15
|
-
if (conflictSetDigest !== composed.conflictSetDigest) throw new Error("SCOPE_CONFLICT_DIGEST_MISMATCH");
|
|
16
|
-
const compositionDigest = digest({ chain: composed.chain, permissions: composed.permissions, delegationIds: [...composed.delegationIds ?? []].sort(), conflictSetDigest, effectiveRevision: composed.effectiveRevision ?? null, sourceRevisionSetDigest: composed.sourceRevisionSetDigest ?? null, ancestorHardAuthorityDigest: composed.ancestorHardAuthorityDigest ?? null, effectiveProgramDigest: composed.effectiveProgramDigest ?? null });
|
|
17
|
-
if (compositionDigest !== composed.compositionDigest) throw new Error("SCOPE_COMPOSITION_DIGEST_MISMATCH");
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ../runtime/src/mcp-gateway.ts
|
|
22
|
-
var MCP_PROTOCOL_VERSIONS = ["2026-07-28", "2025-11-25", "2025-03-26"];
|
|
23
|
-
function negotiateMcpProtocolVersion(offered) {
|
|
24
|
-
const value = String(offered ?? "");
|
|
25
|
-
return MCP_PROTOCOL_VERSIONS.includes(value) ? value : "2025-03-26";
|
|
26
|
-
}
|
|
27
|
-
function assertPublicMcpHost(hostname) {
|
|
28
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
29
|
-
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) throw new Error("MCP_UPSTREAM_PRIVATE_HOST");
|
|
30
|
-
const version = isIP(host);
|
|
31
|
-
if (version === 4) {
|
|
32
|
-
const octets = host.split(".").map(Number);
|
|
33
|
-
if (octets[0] === 10 || octets[0] === 127 || octets[0] === 0 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31 || octets[0] === 192 && octets[1] === 168 || octets[0] === 169 && octets[1] === 254) throw new Error("MCP_UPSTREAM_PRIVATE_HOST");
|
|
34
|
-
}
|
|
35
|
-
if (version === 6 && ["::1", "::", "fc", "fd", "fe8", "fe9", "fea", "feb"].some((prefix) => host === prefix || host.startsWith(`${prefix}:`))) throw new Error("MCP_UPSTREAM_PRIVATE_HOST");
|
|
36
|
-
}
|
|
37
|
-
function canonicalMcpJson(value) {
|
|
38
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
39
|
-
if (Array.isArray(value)) return `[${value.map(canonicalMcpJson).join(",")}]`;
|
|
40
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalMcpJson(value[key])}`).join(",")}}`;
|
|
41
|
-
}
|
|
42
|
-
function verifyMcpResult(result) {
|
|
43
|
-
const { provenanceDigest, ...body } = result;
|
|
44
|
-
const expected = createHash2("sha256").update(canonicalMcpJson(body)).digest("hex");
|
|
45
|
-
if (provenanceDigest !== expected) throw new Error("MCP_PROVENANCE_DIGEST_INVALID");
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
function createMcpCancellationRegistry() {
|
|
49
|
-
const controllers = /* @__PURE__ */ new Map();
|
|
50
|
-
return { register(requestId) {
|
|
51
|
-
if (!requestId.trim()) throw new Error("MCP_REQUEST_ID_REQUIRED");
|
|
52
|
-
const previous = controllers.get(requestId);
|
|
53
|
-
previous?.abort();
|
|
54
|
-
const controller = new AbortController();
|
|
55
|
-
controllers.set(requestId, controller);
|
|
56
|
-
return controller.signal;
|
|
57
|
-
}, cancel(requestId) {
|
|
58
|
-
const controller = controllers.get(requestId);
|
|
59
|
-
if (!controller) return false;
|
|
60
|
-
controller.abort();
|
|
61
|
-
return true;
|
|
62
|
-
}, unregister(requestId, signal) {
|
|
63
|
-
const controller = controllers.get(requestId);
|
|
64
|
-
if (controller && (!signal || controller.signal === signal)) controllers.delete(requestId);
|
|
65
|
-
} };
|
|
66
|
-
}
|
|
67
|
-
function combineMcpAbortSignals(...signals) {
|
|
68
|
-
const active = signals.filter((signal) => Boolean(signal));
|
|
69
|
-
if (active.length === 0) return void 0;
|
|
70
|
-
if (active.length === 1) return active[0];
|
|
71
|
-
const controller = new AbortController();
|
|
72
|
-
const abort = () => controller.abort();
|
|
73
|
-
for (const signal of active) {
|
|
74
|
-
if (signal.aborted) {
|
|
75
|
-
controller.abort();
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
79
|
-
}
|
|
80
|
-
return controller.signal;
|
|
81
|
-
}
|
|
82
|
-
function parseMcpEventStream(body, expectedId) {
|
|
83
|
-
const payloads = [];
|
|
84
|
-
for (const event of body.split(/\r?\n\r?\n/)) {
|
|
85
|
-
const data = [];
|
|
86
|
-
for (const line of event.split(/\r?\n/)) {
|
|
87
|
-
if (!line || line.startsWith(":")) continue;
|
|
88
|
-
const separator = line.indexOf(":");
|
|
89
|
-
const field = separator < 0 ? line : line.slice(0, separator);
|
|
90
|
-
if (field !== "data") continue;
|
|
91
|
-
const value = line.slice(separator + 1).startsWith(" ") ? line.slice(separator + 2) : line.slice(separator + 1);
|
|
92
|
-
data.push(value);
|
|
93
|
-
}
|
|
94
|
-
if (data.length) payloads.push(data.join("\n"));
|
|
95
|
-
}
|
|
96
|
-
return payloads;
|
|
97
|
-
}
|
|
98
|
-
async function readMcpResponseBody(response) {
|
|
99
|
-
if (!response.body) return response.text();
|
|
100
|
-
const reader = response.body.getReader();
|
|
101
|
-
const decoder = new TextDecoder();
|
|
102
|
-
let body = "";
|
|
103
|
-
try {
|
|
104
|
-
for (; ; ) {
|
|
105
|
-
const chunk = await reader.read();
|
|
106
|
-
body += decoder.decode(chunk.value ?? new Uint8Array(), { stream: !chunk.done });
|
|
107
|
-
if (chunk.done) return body;
|
|
108
|
-
}
|
|
109
|
-
} finally {
|
|
110
|
-
reader.releaseLock();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function parseMcpResponseBody(body, contentType, expectedId) {
|
|
114
|
-
const payloads = contentType.toLowerCase().includes("text/event-stream") ? parseMcpEventStream(body, expectedId) : [body];
|
|
115
|
-
let last;
|
|
116
|
-
for (const payload of payloads) {
|
|
117
|
-
try {
|
|
118
|
-
const parsed = JSON.parse(payload);
|
|
119
|
-
last = parsed;
|
|
120
|
-
} catch {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (!last) return { error: "MCP_UPSTREAM_RESPONSE_INVALID" };
|
|
125
|
-
if (last.id !== void 0 && String(last.id) !== expectedId) return { error: "MCP_UPSTREAM_RESPONSE_ID_MISMATCH" };
|
|
126
|
-
return last.error ? { error: last.error.message ?? "MCP_UPSTREAM_ERROR" } : { result: last.result };
|
|
127
|
-
}
|
|
128
|
-
function createRegisteredMcpTransport(input) {
|
|
129
|
-
const url = new URL(input.endpoint);
|
|
130
|
-
if (url.protocol !== "https:") throw new Error("MCP_UPSTREAM_HTTPS_REQUIRED");
|
|
131
|
-
assertPublicMcpHost(url.hostname);
|
|
132
|
-
if (!input.allowedHosts.includes(url.host)) throw new Error("MCP_UPSTREAM_HOST_NOT_ALLOWED");
|
|
133
|
-
const fetcher = input.fetcher ?? fetch;
|
|
134
|
-
const timeoutMs = input.timeoutMs ?? 1e4;
|
|
135
|
-
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) throw new Error("MCP_UPSTREAM_TIMEOUT_INVALID");
|
|
136
|
-
const protocolVersion = input.protocolVersion ?? "2025-03-26";
|
|
137
|
-
return { call: async ({ server, tool, arguments: args, signal }) => {
|
|
138
|
-
const controller = new AbortController();
|
|
139
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
140
|
-
const abort = () => controller.abort();
|
|
141
|
-
if (signal?.aborted) controller.abort();
|
|
142
|
-
else signal?.addEventListener("abort", abort, { once: true });
|
|
143
|
-
try {
|
|
144
|
-
const response = await fetcher(url, { method: "POST", headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "mcp-protocol-version": protocolVersion, ...input.headers }, signal: controller.signal, body: JSON.stringify({ jsonrpc: "2.0", id: `${server}:${tool}`, method: "tools/call", params: { name: tool, arguments: args } }) });
|
|
145
|
-
if (!response.ok) return { error: `MCP_UPSTREAM_HTTP_${response.status}` };
|
|
146
|
-
return parseMcpResponseBody(await readMcpResponseBody(response), response.headers.get("content-type") ?? "application/json", `${server}:${tool}`);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return { error: error instanceof Error && error.name === "AbortError" ? signal?.aborted ? "MCP_REQUEST_CANCELLED" : "MCP_UPSTREAM_TIMEOUT" : "MCP_UPSTREAM_TRANSPORT_ERROR" };
|
|
149
|
-
} finally {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
signal?.removeEventListener("abort", abort);
|
|
152
|
-
}
|
|
153
|
-
} };
|
|
154
|
-
}
|
|
155
|
-
async function handleMcpJsonRpc(input) {
|
|
156
|
-
const { request, server, tools, transport } = input;
|
|
157
|
-
if (request.jsonrpc !== "2.0" || !request.method) throw new Error("MCP_JSONRPC_INVALID");
|
|
158
|
-
if (request.method === "initialize") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2025-03-26", serverInfo: { name: server, version: "1" }, capabilities: { tools: {}, resources: input.resources ? {} : void 0, prompts: input.prompts ? {} : void 0 } } };
|
|
159
|
-
if (request.method === "notifications/initialized") return null;
|
|
160
|
-
if (request.method === "notifications/cancelled") {
|
|
161
|
-
const requestId2 = String(request.params?.requestId ?? "");
|
|
162
|
-
input.cancellationRegistry?.cancel(requestId2);
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
if (request.method === "tools/list") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { tools } };
|
|
166
|
-
if (request.method === "resources/list") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { resources: input.resources ?? [] } };
|
|
167
|
-
if (request.method === "prompts/list") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { prompts: input.prompts ?? [] } };
|
|
168
|
-
if (request.method === "resources/read") {
|
|
169
|
-
const value = input.readResource ? await input.readResource(String(request.params?.uri ?? "")) : null;
|
|
170
|
-
return request.id === void 0 ? null : value ? { jsonrpc: "2.0", id: request.id, result: value } : { jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "RESOURCE_NOT_FOUND" } };
|
|
171
|
-
}
|
|
172
|
-
if (request.method === "prompts/get") {
|
|
173
|
-
const value = input.getPrompt ? await input.getPrompt(String(request.params?.name ?? ""), request.params?.arguments ?? {}) : null;
|
|
174
|
-
return request.id === void 0 ? null : value ? { jsonrpc: "2.0", id: request.id, result: value } : { jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "PROMPT_NOT_FOUND" } };
|
|
175
|
-
}
|
|
176
|
-
if (request.method !== "tools/call") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "METHOD_NOT_FOUND" } };
|
|
177
|
-
const tool = String(request.params?.name ?? "");
|
|
178
|
-
const definition = tools.find((item) => item.name === tool);
|
|
179
|
-
if (!definition) return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "TOOL_NOT_FOUND" } };
|
|
180
|
-
const invocation = { ...input.invocation, server, tool, arguments: request.params?.arguments ?? {} };
|
|
181
|
-
const authority = definition.effectful ? input.authorize ? await input.authorize({ invocation, definition }) : { allowed: false, decisionDigest: "", reason: "MCP_AUTHORITY_REQUIRED" } : { allowed: true, decisionDigest: "read-only" };
|
|
182
|
-
if (!authority.allowed) return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, error: { code: -32003, message: authority.reason ?? "MCP_AUTHORITY_DENIED", data: { decisionDigest: authority.decisionDigest } } };
|
|
183
|
-
const requestId = String(request.id ?? "");
|
|
184
|
-
const requestSignal = input.cancellationRegistry && requestId ? input.cancellationRegistry.register(requestId) : void 0;
|
|
185
|
-
try {
|
|
186
|
-
const result = await invokeMcpTool({ invocation, transport, authority, signal: combineMcpAbortSignals(input.signal, requestSignal) });
|
|
187
|
-
return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result.result ?? result.error ?? null) }], isError: !result.ok, provenanceDigest: result.provenanceDigest } };
|
|
188
|
-
} finally {
|
|
189
|
-
if (requestSignal) input.cancellationRegistry?.unregister(requestId, requestSignal);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async function invokeMcpTool(input) {
|
|
193
|
-
const { invocation, transport } = input;
|
|
194
|
-
if (!invocation.id || !invocation.server || !invocation.tool || !invocation.receiptId || !invocation.idempotencyKey) throw new Error("MCP_INVOCATION_FIELDS_REQUIRED");
|
|
195
|
-
if (invocation.authorityDigest !== void 0 && !/^[a-f0-9]{64}$/.test(invocation.authorityDigest)) throw new Error("MCP_AUTHORITY_DIGEST_INVALID");
|
|
196
|
-
if (invocation.composedScope) {
|
|
197
|
-
assertComposedScopeIntegrity(invocation.composedScope);
|
|
198
|
-
if (invocation.authorityDigest !== void 0 && invocation.authorityDigest !== invocation.composedScope.compositionDigest) throw new Error("MCP_AUTHORITY_MISMATCH");
|
|
199
|
-
if (!invocation.composedScope.serveable) throw new Error("MCP_SCOPE_COMPOSITION_CONFLICT");
|
|
200
|
-
}
|
|
201
|
-
if (input.authority && !input.authority.allowed) throw new Error("MCP_AUTHORITY_DENIED");
|
|
202
|
-
if (!transport || typeof transport.call !== "function") throw new Error("MCP_TRANSPORT_REQUIRED");
|
|
203
|
-
let response;
|
|
204
|
-
try {
|
|
205
|
-
response = await transport.call({ server: invocation.server, tool: invocation.tool, arguments: structuredClone(invocation.arguments), signal: input.signal });
|
|
206
|
-
} catch {
|
|
207
|
-
if (input.signal?.aborted) throw new Error("MCP_INVOCATION_CANCELLED");
|
|
208
|
-
response = { error: "MCP_TRANSPORT_ERROR" };
|
|
209
|
-
}
|
|
210
|
-
const argumentsDigest = createHash2("sha256").update(canonicalMcpJson(invocation.arguments)).digest("hex");
|
|
211
|
-
const body = { invocationId: invocation.id, organizationId: invocation.organizationId, projectId: invocation.projectId, environmentId: invocation.environmentId, server: invocation.server, tool: invocation.tool, receiptId: invocation.receiptId, idempotencyKey: invocation.idempotencyKey, argumentsDigest, authorityDecisionDigest: input.authority?.decisionDigest ?? "not-required", authorityDigest: invocation.authorityDigest ?? null, ok: !response.error, result: response.result, error: response.error };
|
|
212
|
-
return Object.freeze({ ...body, provenanceDigest: createHash2("sha256").update(canonicalMcpJson(body)).digest("hex") });
|
|
213
|
-
}
|
|
214
|
-
export {
|
|
215
|
-
MCP_PROTOCOL_VERSIONS,
|
|
216
|
-
canonicalMcpJson,
|
|
217
|
-
createMcpCancellationRegistry,
|
|
218
|
-
createRegisteredMcpTransport,
|
|
219
|
-
handleMcpJsonRpc,
|
|
220
|
-
invokeMcpTool,
|
|
221
|
-
negotiateMcpProtocolVersion,
|
|
222
|
-
parseMcpEventStream,
|
|
223
|
-
verifyMcpResult
|
|
224
|
-
};
|
|
1
|
+
// ../runtime/src/mcp-gateway.ts
|
|
2
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
|
|
5
|
+
// ../domain/src/scope-composer.ts
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
function canonical(value) {
|
|
8
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
9
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
10
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
11
|
+
}
|
|
12
|
+
var digest = (value) => createHash("sha256").update(canonical(value)).digest("hex");
|
|
13
|
+
function assertComposedScopeIntegrity(composed) {
|
|
14
|
+
const conflictSetDigest = digest(composed.conflicts);
|
|
15
|
+
if (conflictSetDigest !== composed.conflictSetDigest) throw new Error("SCOPE_CONFLICT_DIGEST_MISMATCH");
|
|
16
|
+
const compositionDigest = digest({ chain: composed.chain, permissions: composed.permissions, delegationIds: [...composed.delegationIds ?? []].sort(), conflictSetDigest, effectiveRevision: composed.effectiveRevision ?? null, sourceRevisionSetDigest: composed.sourceRevisionSetDigest ?? null, ancestorHardAuthorityDigest: composed.ancestorHardAuthorityDigest ?? null, effectiveProgramDigest: composed.effectiveProgramDigest ?? null });
|
|
17
|
+
if (compositionDigest !== composed.compositionDigest) throw new Error("SCOPE_COMPOSITION_DIGEST_MISMATCH");
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ../runtime/src/mcp-gateway.ts
|
|
22
|
+
var MCP_PROTOCOL_VERSIONS = ["2026-07-28", "2025-11-25", "2025-03-26"];
|
|
23
|
+
function negotiateMcpProtocolVersion(offered) {
|
|
24
|
+
const value = String(offered ?? "");
|
|
25
|
+
return MCP_PROTOCOL_VERSIONS.includes(value) ? value : "2025-03-26";
|
|
26
|
+
}
|
|
27
|
+
function assertPublicMcpHost(hostname) {
|
|
28
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
29
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) throw new Error("MCP_UPSTREAM_PRIVATE_HOST");
|
|
30
|
+
const version = isIP(host);
|
|
31
|
+
if (version === 4) {
|
|
32
|
+
const octets = host.split(".").map(Number);
|
|
33
|
+
if (octets[0] === 10 || octets[0] === 127 || octets[0] === 0 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31 || octets[0] === 192 && octets[1] === 168 || octets[0] === 169 && octets[1] === 254) throw new Error("MCP_UPSTREAM_PRIVATE_HOST");
|
|
34
|
+
}
|
|
35
|
+
if (version === 6 && ["::1", "::", "fc", "fd", "fe8", "fe9", "fea", "feb"].some((prefix) => host === prefix || host.startsWith(`${prefix}:`))) throw new Error("MCP_UPSTREAM_PRIVATE_HOST");
|
|
36
|
+
}
|
|
37
|
+
function canonicalMcpJson(value) {
|
|
38
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
39
|
+
if (Array.isArray(value)) return `[${value.map(canonicalMcpJson).join(",")}]`;
|
|
40
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalMcpJson(value[key])}`).join(",")}}`;
|
|
41
|
+
}
|
|
42
|
+
function verifyMcpResult(result) {
|
|
43
|
+
const { provenanceDigest, ...body } = result;
|
|
44
|
+
const expected = createHash2("sha256").update(canonicalMcpJson(body)).digest("hex");
|
|
45
|
+
if (provenanceDigest !== expected) throw new Error("MCP_PROVENANCE_DIGEST_INVALID");
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function createMcpCancellationRegistry() {
|
|
49
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
50
|
+
return { register(requestId) {
|
|
51
|
+
if (!requestId.trim()) throw new Error("MCP_REQUEST_ID_REQUIRED");
|
|
52
|
+
const previous = controllers.get(requestId);
|
|
53
|
+
previous?.abort();
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
controllers.set(requestId, controller);
|
|
56
|
+
return controller.signal;
|
|
57
|
+
}, cancel(requestId) {
|
|
58
|
+
const controller = controllers.get(requestId);
|
|
59
|
+
if (!controller) return false;
|
|
60
|
+
controller.abort();
|
|
61
|
+
return true;
|
|
62
|
+
}, unregister(requestId, signal) {
|
|
63
|
+
const controller = controllers.get(requestId);
|
|
64
|
+
if (controller && (!signal || controller.signal === signal)) controllers.delete(requestId);
|
|
65
|
+
} };
|
|
66
|
+
}
|
|
67
|
+
function combineMcpAbortSignals(...signals) {
|
|
68
|
+
const active = signals.filter((signal) => Boolean(signal));
|
|
69
|
+
if (active.length === 0) return void 0;
|
|
70
|
+
if (active.length === 1) return active[0];
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const abort = () => controller.abort();
|
|
73
|
+
for (const signal of active) {
|
|
74
|
+
if (signal.aborted) {
|
|
75
|
+
controller.abort();
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
79
|
+
}
|
|
80
|
+
return controller.signal;
|
|
81
|
+
}
|
|
82
|
+
function parseMcpEventStream(body, expectedId) {
|
|
83
|
+
const payloads = [];
|
|
84
|
+
for (const event of body.split(/\r?\n\r?\n/)) {
|
|
85
|
+
const data = [];
|
|
86
|
+
for (const line of event.split(/\r?\n/)) {
|
|
87
|
+
if (!line || line.startsWith(":")) continue;
|
|
88
|
+
const separator = line.indexOf(":");
|
|
89
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
90
|
+
if (field !== "data") continue;
|
|
91
|
+
const value = line.slice(separator + 1).startsWith(" ") ? line.slice(separator + 2) : line.slice(separator + 1);
|
|
92
|
+
data.push(value);
|
|
93
|
+
}
|
|
94
|
+
if (data.length) payloads.push(data.join("\n"));
|
|
95
|
+
}
|
|
96
|
+
return payloads;
|
|
97
|
+
}
|
|
98
|
+
async function readMcpResponseBody(response) {
|
|
99
|
+
if (!response.body) return response.text();
|
|
100
|
+
const reader = response.body.getReader();
|
|
101
|
+
const decoder = new TextDecoder();
|
|
102
|
+
let body = "";
|
|
103
|
+
try {
|
|
104
|
+
for (; ; ) {
|
|
105
|
+
const chunk = await reader.read();
|
|
106
|
+
body += decoder.decode(chunk.value ?? new Uint8Array(), { stream: !chunk.done });
|
|
107
|
+
if (chunk.done) return body;
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
reader.releaseLock();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function parseMcpResponseBody(body, contentType, expectedId) {
|
|
114
|
+
const payloads = contentType.toLowerCase().includes("text/event-stream") ? parseMcpEventStream(body, expectedId) : [body];
|
|
115
|
+
let last;
|
|
116
|
+
for (const payload of payloads) {
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(payload);
|
|
119
|
+
last = parsed;
|
|
120
|
+
} catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!last) return { error: "MCP_UPSTREAM_RESPONSE_INVALID" };
|
|
125
|
+
if (last.id !== void 0 && String(last.id) !== expectedId) return { error: "MCP_UPSTREAM_RESPONSE_ID_MISMATCH" };
|
|
126
|
+
return last.error ? { error: last.error.message ?? "MCP_UPSTREAM_ERROR" } : { result: last.result };
|
|
127
|
+
}
|
|
128
|
+
function createRegisteredMcpTransport(input) {
|
|
129
|
+
const url = new URL(input.endpoint);
|
|
130
|
+
if (url.protocol !== "https:") throw new Error("MCP_UPSTREAM_HTTPS_REQUIRED");
|
|
131
|
+
assertPublicMcpHost(url.hostname);
|
|
132
|
+
if (!input.allowedHosts.includes(url.host)) throw new Error("MCP_UPSTREAM_HOST_NOT_ALLOWED");
|
|
133
|
+
const fetcher = input.fetcher ?? fetch;
|
|
134
|
+
const timeoutMs = input.timeoutMs ?? 1e4;
|
|
135
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) throw new Error("MCP_UPSTREAM_TIMEOUT_INVALID");
|
|
136
|
+
const protocolVersion = input.protocolVersion ?? "2025-03-26";
|
|
137
|
+
return { call: async ({ server, tool, arguments: args, signal }) => {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
140
|
+
const abort = () => controller.abort();
|
|
141
|
+
if (signal?.aborted) controller.abort();
|
|
142
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetcher(url, { method: "POST", headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "mcp-protocol-version": protocolVersion, ...input.headers }, signal: controller.signal, body: JSON.stringify({ jsonrpc: "2.0", id: `${server}:${tool}`, method: "tools/call", params: { name: tool, arguments: args } }) });
|
|
145
|
+
if (!response.ok) return { error: `MCP_UPSTREAM_HTTP_${response.status}` };
|
|
146
|
+
return parseMcpResponseBody(await readMcpResponseBody(response), response.headers.get("content-type") ?? "application/json", `${server}:${tool}`);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { error: error instanceof Error && error.name === "AbortError" ? signal?.aborted ? "MCP_REQUEST_CANCELLED" : "MCP_UPSTREAM_TIMEOUT" : "MCP_UPSTREAM_TRANSPORT_ERROR" };
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
signal?.removeEventListener("abort", abort);
|
|
152
|
+
}
|
|
153
|
+
} };
|
|
154
|
+
}
|
|
155
|
+
async function handleMcpJsonRpc(input) {
|
|
156
|
+
const { request, server, tools, transport } = input;
|
|
157
|
+
if (request.jsonrpc !== "2.0" || !request.method) throw new Error("MCP_JSONRPC_INVALID");
|
|
158
|
+
if (request.method === "initialize") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2025-03-26", serverInfo: { name: server, version: "1" }, capabilities: { tools: {}, resources: input.resources ? {} : void 0, prompts: input.prompts ? {} : void 0 } } };
|
|
159
|
+
if (request.method === "notifications/initialized") return null;
|
|
160
|
+
if (request.method === "notifications/cancelled") {
|
|
161
|
+
const requestId2 = String(request.params?.requestId ?? "");
|
|
162
|
+
input.cancellationRegistry?.cancel(requestId2);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (request.method === "tools/list") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { tools } };
|
|
166
|
+
if (request.method === "resources/list") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { resources: input.resources ?? [] } };
|
|
167
|
+
if (request.method === "prompts/list") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { prompts: input.prompts ?? [] } };
|
|
168
|
+
if (request.method === "resources/read") {
|
|
169
|
+
const value = input.readResource ? await input.readResource(String(request.params?.uri ?? "")) : null;
|
|
170
|
+
return request.id === void 0 ? null : value ? { jsonrpc: "2.0", id: request.id, result: value } : { jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "RESOURCE_NOT_FOUND" } };
|
|
171
|
+
}
|
|
172
|
+
if (request.method === "prompts/get") {
|
|
173
|
+
const value = input.getPrompt ? await input.getPrompt(String(request.params?.name ?? ""), request.params?.arguments ?? {}) : null;
|
|
174
|
+
return request.id === void 0 ? null : value ? { jsonrpc: "2.0", id: request.id, result: value } : { jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "PROMPT_NOT_FOUND" } };
|
|
175
|
+
}
|
|
176
|
+
if (request.method !== "tools/call") return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "METHOD_NOT_FOUND" } };
|
|
177
|
+
const tool = String(request.params?.name ?? "");
|
|
178
|
+
const definition = tools.find((item) => item.name === tool);
|
|
179
|
+
if (!definition) return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "TOOL_NOT_FOUND" } };
|
|
180
|
+
const invocation = { ...input.invocation, server, tool, arguments: request.params?.arguments ?? {} };
|
|
181
|
+
const authority = definition.effectful ? input.authorize ? await input.authorize({ invocation, definition }) : { allowed: false, decisionDigest: "", reason: "MCP_AUTHORITY_REQUIRED" } : { allowed: true, decisionDigest: "read-only" };
|
|
182
|
+
if (!authority.allowed) return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, error: { code: -32003, message: authority.reason ?? "MCP_AUTHORITY_DENIED", data: { decisionDigest: authority.decisionDigest } } };
|
|
183
|
+
const requestId = String(request.id ?? "");
|
|
184
|
+
const requestSignal = input.cancellationRegistry && requestId ? input.cancellationRegistry.register(requestId) : void 0;
|
|
185
|
+
try {
|
|
186
|
+
const result = await invokeMcpTool({ invocation, transport, authority, signal: combineMcpAbortSignals(input.signal, requestSignal) });
|
|
187
|
+
return request.id === void 0 ? null : { jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result.result ?? result.error ?? null) }], isError: !result.ok, provenanceDigest: result.provenanceDigest } };
|
|
188
|
+
} finally {
|
|
189
|
+
if (requestSignal) input.cancellationRegistry?.unregister(requestId, requestSignal);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function invokeMcpTool(input) {
|
|
193
|
+
const { invocation, transport } = input;
|
|
194
|
+
if (!invocation.id || !invocation.server || !invocation.tool || !invocation.receiptId || !invocation.idempotencyKey) throw new Error("MCP_INVOCATION_FIELDS_REQUIRED");
|
|
195
|
+
if (invocation.authorityDigest !== void 0 && !/^[a-f0-9]{64}$/.test(invocation.authorityDigest)) throw new Error("MCP_AUTHORITY_DIGEST_INVALID");
|
|
196
|
+
if (invocation.composedScope) {
|
|
197
|
+
assertComposedScopeIntegrity(invocation.composedScope);
|
|
198
|
+
if (invocation.authorityDigest !== void 0 && invocation.authorityDigest !== invocation.composedScope.compositionDigest) throw new Error("MCP_AUTHORITY_MISMATCH");
|
|
199
|
+
if (!invocation.composedScope.serveable) throw new Error("MCP_SCOPE_COMPOSITION_CONFLICT");
|
|
200
|
+
}
|
|
201
|
+
if (input.authority && !input.authority.allowed) throw new Error("MCP_AUTHORITY_DENIED");
|
|
202
|
+
if (!transport || typeof transport.call !== "function") throw new Error("MCP_TRANSPORT_REQUIRED");
|
|
203
|
+
let response;
|
|
204
|
+
try {
|
|
205
|
+
response = await transport.call({ server: invocation.server, tool: invocation.tool, arguments: structuredClone(invocation.arguments), signal: input.signal });
|
|
206
|
+
} catch {
|
|
207
|
+
if (input.signal?.aborted) throw new Error("MCP_INVOCATION_CANCELLED");
|
|
208
|
+
response = { error: "MCP_TRANSPORT_ERROR" };
|
|
209
|
+
}
|
|
210
|
+
const argumentsDigest = createHash2("sha256").update(canonicalMcpJson(invocation.arguments)).digest("hex");
|
|
211
|
+
const body = { invocationId: invocation.id, organizationId: invocation.organizationId, projectId: invocation.projectId, environmentId: invocation.environmentId, server: invocation.server, tool: invocation.tool, receiptId: invocation.receiptId, idempotencyKey: invocation.idempotencyKey, argumentsDigest, authorityDecisionDigest: input.authority?.decisionDigest ?? "not-required", authorityDigest: invocation.authorityDigest ?? null, ok: !response.error, result: response.result, error: response.error };
|
|
212
|
+
return Object.freeze({ ...body, provenanceDigest: createHash2("sha256").update(canonicalMcpJson(body)).digest("hex") });
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
MCP_PROTOCOL_VERSIONS,
|
|
216
|
+
canonicalMcpJson,
|
|
217
|
+
createMcpCancellationRegistry,
|
|
218
|
+
createRegisteredMcpTransport,
|
|
219
|
+
handleMcpJsonRpc,
|
|
220
|
+
invokeMcpTool,
|
|
221
|
+
negotiateMcpProtocolVersion,
|
|
222
|
+
parseMcpEventStream,
|
|
223
|
+
verifyMcpResult
|
|
224
|
+
};
|