@agent-vm/mcp-portal 0.0.69 → 0.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -11
- package/dist/agent-bearer-token-DCtpDPCZ.js +59 -0
- package/dist/agent-bearer-token-DCtpDPCZ.js.map +1 -0
- package/dist/bin/mcp-portal.d.ts +28 -0
- package/dist/bin/mcp-portal.d.ts.map +1 -0
- package/dist/bin/mcp-portal.js +318 -0
- package/dist/bin/mcp-portal.js.map +1 -0
- package/dist/{catalog-types--gUGFPpN.d.ts → catalog-types-BVuB4Ynx.d.ts} +1 -1
- package/dist/{catalog-types--gUGFPpN.d.ts.map → catalog-types-BVuB4Ynx.d.ts.map} +1 -1
- package/dist/cli/index.d.ts +101 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/hmac-env-B4shpRRB.js +20 -0
- package/dist/hmac-env-B4shpRRB.js.map +1 -0
- package/dist/hmac-token-DBqWY3-w.js +100 -0
- package/dist/hmac-token-DBqWY3-w.js.map +1 -0
- package/dist/index.d.ts +5 -485
- package/dist/index.js +4 -5
- package/dist/mcp-proxy/index.d.ts +24 -0
- package/dist/mcp-proxy/index.d.ts.map +1 -0
- package/dist/mcp-proxy/index.js +2 -0
- package/dist/portal-auth/agent-bearer-token.d.ts +22 -0
- package/dist/portal-auth/agent-bearer-token.d.ts.map +1 -0
- package/dist/portal-auth/agent-bearer-token.js +2 -0
- package/dist/portal-auth/hmac-env.d.ts +6 -0
- package/dist/portal-auth/hmac-env.d.ts.map +1 -0
- package/dist/portal-auth/hmac-env.js +2 -0
- package/dist/portal-auth/hmac-token.d.ts +40 -0
- package/dist/portal-auth/hmac-token.d.ts.map +1 -0
- package/dist/portal-auth/hmac-token.js +2 -0
- package/dist/portal-config/index.d.ts +11 -0
- package/dist/portal-config/index.d.ts.map +1 -0
- package/dist/{tool-vm → portal-config}/index.js +2 -3
- package/dist/portal-core-CZQI7Ob6.d.ts +264 -0
- package/dist/portal-core-CZQI7Ob6.d.ts.map +1 -0
- package/dist/portal-core-Cgu714CL.js +416 -0
- package/dist/portal-core-Cgu714CL.js.map +1 -0
- package/dist/portal-session-DG2CUjIo.d.ts +184 -0
- package/dist/portal-session-DG2CUjIo.d.ts.map +1 -0
- package/dist/portal-tools-DKci1szO.js +528 -0
- package/dist/portal-tools-DKci1szO.js.map +1 -0
- package/dist/resolve-agent-identity-DnC_Pmnh.js +550 -0
- package/dist/resolve-agent-identity-DnC_Pmnh.js.map +1 -0
- package/dist/resolve-agent-identity-FQL02YdW.d.ts +81 -0
- package/dist/resolve-agent-identity-FQL02YdW.d.ts.map +1 -0
- package/dist/serve-command-CnSMUybd.js +358 -0
- package/dist/serve-command-CnSMUybd.js.map +1 -0
- package/dist/testing/fake-upstream-mcp-server.d.ts +5 -2
- package/dist/testing/fake-upstream-mcp-server.d.ts.map +1 -1
- package/dist/testing/fake-upstream-mcp-server.js +14 -4
- package/dist/testing/fake-upstream-mcp-server.js.map +1 -1
- package/dist/typescript-artifact-BVLt3Ifd.js +60 -0
- package/dist/typescript-artifact-BVLt3Ifd.js.map +1 -0
- package/dist/upstream-mcp-client-runtime-JlsfTm7_.js +760 -0
- package/dist/upstream-mcp-client-runtime-JlsfTm7_.js.map +1 -0
- package/dist/upstream-response-middleware-1MZnAD9C.d.ts +115 -0
- package/dist/upstream-response-middleware-1MZnAD9C.d.ts.map +1 -0
- package/dist/upstream-response-middleware-BjUWZ2G8.js +172 -0
- package/dist/upstream-response-middleware-BjUWZ2G8.js.map +1 -0
- package/dist/{index-BcI9c8sg.d.ts → zod-schema-loader-DLGQpYFD.d.ts} +3 -9
- package/dist/zod-schema-loader-DLGQpYFD.d.ts.map +1 -0
- package/dist/{typescript-artifact-BqU8okQy.js → zod-schema-loader-yNekKNpm.js} +85 -55
- package/dist/zod-schema-loader-yNekKNpm.js.map +1 -0
- package/package.json +30 -13
- package/dist/bin/agent-vm-mcp-portal.d.ts +0 -10
- package/dist/bin/agent-vm-mcp-portal.d.ts.map +0 -1
- package/dist/bin/agent-vm-mcp-portal.js +0 -56
- package/dist/bin/agent-vm-mcp-portal.js.map +0 -1
- package/dist/bin/portal-server.d.ts +0 -55
- package/dist/bin/portal-server.d.ts.map +0 -1
- package/dist/bin/portal-server.js +0 -289
- package/dist/bin/portal-server.js.map +0 -1
- package/dist/index-BcI9c8sg.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/tool-vm/index.d.ts +0 -2
- package/dist/tool-vm-ihnzDyjJ.js +0 -3
- package/dist/typescript-artifact-BqU8okQy.js.map +0 -1
- package/dist/upstream-mcp-client-runtime-DiBCBsDj.js +0 -1729
- package/dist/upstream-mcp-client-runtime-DiBCBsDj.js.map +0 -1
- package/dist/zod-schema-loader-CDDtoRE1.js +0 -90
- package/dist/zod-schema-loader-CDDtoRE1.js.map +0 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { i as redactThrownError, u as createPortalAgentIdentity } from "./upstream-response-middleware-BjUWZ2G8.js";
|
|
2
|
+
import { n as portalToolInputSchemas } from "./portal-tools-DKci1szO.js";
|
|
3
|
+
import { i as verifyAgentBearerAuthorization } from "./agent-bearer-token-DCtpDPCZ.js";
|
|
4
|
+
import { r as verifyApprovalToken, t as hashCallArguments } from "./hmac-token-DBqWY3-w.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { mcpPortalCallRequiresApproval, resolveMcpPortalProfile } from "@agent-vm/config-contracts";
|
|
8
|
+
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
9
|
+
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
//#region src/mcp-proxy/portal-mcp-server.ts
|
|
13
|
+
const portalMcpToolNames = [
|
|
14
|
+
"mcp_portal_list",
|
|
15
|
+
"mcp_portal_search",
|
|
16
|
+
"mcp_portal_describe",
|
|
17
|
+
"mcp_portal_call"
|
|
18
|
+
];
|
|
19
|
+
const portalMcpToolNameSet = new Set(portalMcpToolNames);
|
|
20
|
+
function isPortalCoreToolName(value) {
|
|
21
|
+
return portalMcpToolNameSet.has(value);
|
|
22
|
+
}
|
|
23
|
+
function listPortalMcpTools(descriptors) {
|
|
24
|
+
return descriptors ?? [
|
|
25
|
+
{
|
|
26
|
+
description: "List authorized MCP namespaces and compact tool summaries.",
|
|
27
|
+
inputSchema: portalToolInputSchemas.mcp_portal_list,
|
|
28
|
+
name: "mcp_portal_list"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
description: "Search the caller scoped MCP Portal index.",
|
|
32
|
+
inputSchema: portalToolInputSchemas.mcp_portal_search,
|
|
33
|
+
name: "mcp_portal_search"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
description: "Describe exact MCP tool schemas and optional TypeScript/Zod helpers.",
|
|
37
|
+
inputSchema: portalToolInputSchemas.mcp_portal_describe,
|
|
38
|
+
name: "mcp_portal_describe"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description: "Validate and call an authorized upstream MCP tool by namespace and toolName.",
|
|
42
|
+
inputSchema: portalToolInputSchemas.mcp_portal_call,
|
|
43
|
+
name: "mcp_portal_call"
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function isObjectRecord(value) {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
function coreResultIsError(value) {
|
|
51
|
+
if (value.items.some((item) => item.status === "failed")) return true;
|
|
52
|
+
if (!isObjectRecord(value.structuredContent)) return false;
|
|
53
|
+
return value.structuredContent.ok === false;
|
|
54
|
+
}
|
|
55
|
+
function jsonToolResult(value) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{
|
|
58
|
+
text: JSON.stringify(value),
|
|
59
|
+
type: "text"
|
|
60
|
+
}],
|
|
61
|
+
...coreResultIsError(value) ? { isError: true } : {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function errorToolResult(error) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{
|
|
67
|
+
text: redactThrownError(error).message,
|
|
68
|
+
type: "text"
|
|
69
|
+
}],
|
|
70
|
+
isError: true
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function sendBestEffortNotification(notification, sendNotification) {
|
|
74
|
+
try {
|
|
75
|
+
await sendNotification(notification);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
async function emitMcpProgress(props) {
|
|
79
|
+
if (props.event.kind === "upstream_notification") {
|
|
80
|
+
if (!isObjectRecord(props.event.params)) return;
|
|
81
|
+
if (props.event.method === "notifications/progress") {
|
|
82
|
+
if (props.progressToken === void 0) return;
|
|
83
|
+
await sendBestEffortNotification({
|
|
84
|
+
method: "notifications/progress",
|
|
85
|
+
params: {
|
|
86
|
+
...props.event.params,
|
|
87
|
+
progressToken: props.progressToken
|
|
88
|
+
}
|
|
89
|
+
}, props.sendNotification);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (props.event.method !== "notifications/message") return;
|
|
93
|
+
await sendBestEffortNotification({
|
|
94
|
+
method: "notifications/message",
|
|
95
|
+
params: props.event.params
|
|
96
|
+
}, props.sendNotification);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (props.event.kind !== "progress" && props.event.kind !== "partial_content") return;
|
|
100
|
+
const message = props.event.kind === "progress" ? props.event.message : props.event.content.type === "text" ? props.event.content.text : JSON.stringify(props.event.content.value);
|
|
101
|
+
if (message === void 0 || message.length === 0) return;
|
|
102
|
+
if (props.progressToken !== void 0) {
|
|
103
|
+
await sendBestEffortNotification({
|
|
104
|
+
method: "notifications/progress",
|
|
105
|
+
params: {
|
|
106
|
+
message,
|
|
107
|
+
progress: props.event.kind === "progress" ? props.event.progress ?? 0 : 0,
|
|
108
|
+
progressToken: props.progressToken,
|
|
109
|
+
...props.event.kind === "progress" && props.event.total !== void 0 ? { total: props.event.total } : {}
|
|
110
|
+
}
|
|
111
|
+
}, props.sendNotification);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await sendBestEffortNotification({
|
|
115
|
+
method: "notifications/message",
|
|
116
|
+
params: {
|
|
117
|
+
data: message,
|
|
118
|
+
level: "info"
|
|
119
|
+
}
|
|
120
|
+
}, props.sendNotification);
|
|
121
|
+
}
|
|
122
|
+
function createPortalMcpServer(props) {
|
|
123
|
+
const server = new Server({
|
|
124
|
+
name: "mcp-portal",
|
|
125
|
+
version: "1.0.0"
|
|
126
|
+
}, { capabilities: { tools: { listChanged: false } } });
|
|
127
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listPortalMcpTools(props.core.describeTools(props.scope)) }));
|
|
128
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
129
|
+
if (!isPortalCoreToolName(request.params.name)) return {
|
|
130
|
+
content: [{
|
|
131
|
+
text: `Unknown MCP Portal tool: ${request.params.name}`,
|
|
132
|
+
type: "text"
|
|
133
|
+
}],
|
|
134
|
+
isError: true
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
return jsonToolResult(await props.core.collectPortalCoreResult(props.core.callStream({
|
|
138
|
+
input: request.params.arguments ?? {},
|
|
139
|
+
scope: props.scope,
|
|
140
|
+
signal: extra.signal,
|
|
141
|
+
toolName: request.params.name
|
|
142
|
+
}), { onEvent: async (event) => {
|
|
143
|
+
await emitMcpProgress({
|
|
144
|
+
event,
|
|
145
|
+
progressToken: extra["_meta"]?.progressToken,
|
|
146
|
+
sendNotification: extra.sendNotification
|
|
147
|
+
});
|
|
148
|
+
} }));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return errorToolResult(error);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return server;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/mcp-proxy/portal-http-server.ts
|
|
157
|
+
const mcpSessionIdHeader = "mcp-session-id";
|
|
158
|
+
const defaultAuthFailureLimit = {
|
|
159
|
+
maxFailures: 60,
|
|
160
|
+
windowMs: 6e4
|
|
161
|
+
};
|
|
162
|
+
const authFailureBucketLimit = 1024;
|
|
163
|
+
const directClientAddress = "direct-client";
|
|
164
|
+
const unknownAgentCredentialVersionForOpaqueAuthTiming = 1;
|
|
165
|
+
function activeSessionKey(scopeId, sessionId) {
|
|
166
|
+
return `${scopeId}\n${sessionId}`;
|
|
167
|
+
}
|
|
168
|
+
function unauthorizedResponse() {
|
|
169
|
+
return Response.json({
|
|
170
|
+
error: { kind: "unauthorized" },
|
|
171
|
+
ok: false
|
|
172
|
+
}, { status: 401 });
|
|
173
|
+
}
|
|
174
|
+
function rateLimitedResponse() {
|
|
175
|
+
return Response.json({
|
|
176
|
+
error: { kind: "rate_limited" },
|
|
177
|
+
ok: false
|
|
178
|
+
}, { status: 429 });
|
|
179
|
+
}
|
|
180
|
+
function unavailableResponse() {
|
|
181
|
+
return Response.json({
|
|
182
|
+
error: { kind: "shutting_down" },
|
|
183
|
+
ok: false
|
|
184
|
+
}, { status: 503 });
|
|
185
|
+
}
|
|
186
|
+
function errorFromUnknown(error) {
|
|
187
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
188
|
+
}
|
|
189
|
+
function clientAddressFromContext(context) {
|
|
190
|
+
try {
|
|
191
|
+
const address = getConnInfo(context).remote.address;
|
|
192
|
+
return address && address.length > 0 ? address : directClientAddress;
|
|
193
|
+
} catch {
|
|
194
|
+
return directClientAddress;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function createPortalHttpApp(options) {
|
|
198
|
+
if (options.agentBearerAuth === void 0) throw new Error("MCP Portal HTTP app requires agent bearer auth.");
|
|
199
|
+
if (options.agentBearerAuth.authorizationHeaderName.length === 0) throw new Error("MCP Portal HTTP app requires agent bearer auth header name.");
|
|
200
|
+
const app = new Hono();
|
|
201
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
202
|
+
const authFailureBuckets = /* @__PURE__ */ new Map();
|
|
203
|
+
const pendingNewSessionRequests = /* @__PURE__ */ new Set();
|
|
204
|
+
const authFailureLimit = options.authFailureLimit ?? defaultAuthFailureLimit;
|
|
205
|
+
let closing = false;
|
|
206
|
+
async function auditAuth(event) {
|
|
207
|
+
const auditEvent = {
|
|
208
|
+
...event,
|
|
209
|
+
kind: "mcp_proxy_auth",
|
|
210
|
+
timeMs: Date.now()
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
await options.auditSink?.(auditEvent);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
await options.auditErrorSink?.(errorFromUnknown(error), auditEvent);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function reportSessionCloseError(error, identity) {
|
|
219
|
+
await options.onSessionCloseError?.(errorFromUnknown(error), identity);
|
|
220
|
+
}
|
|
221
|
+
function pruneAuthFailureBuckets(nowMs) {
|
|
222
|
+
for (const [key, bucket] of authFailureBuckets) if (bucket.resetAtMs <= nowMs) authFailureBuckets.delete(key);
|
|
223
|
+
while (authFailureBuckets.size > authFailureBucketLimit) {
|
|
224
|
+
const firstKey = authFailureBuckets.keys().next().value;
|
|
225
|
+
if (typeof firstKey !== "string") return;
|
|
226
|
+
authFailureBuckets.delete(firstKey);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function isAuthFailureRateLimited(clientAddress) {
|
|
230
|
+
const nowMs = Date.now();
|
|
231
|
+
pruneAuthFailureBuckets(nowMs);
|
|
232
|
+
const bucket = authFailureBuckets.get(clientAddress);
|
|
233
|
+
if (bucket === void 0) return false;
|
|
234
|
+
if (bucket.resetAtMs <= nowMs) {
|
|
235
|
+
authFailureBuckets.delete(clientAddress);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return bucket.failures >= authFailureLimit.maxFailures;
|
|
239
|
+
}
|
|
240
|
+
function recordAuthFailure(clientAddress) {
|
|
241
|
+
const nowMs = Date.now();
|
|
242
|
+
pruneAuthFailureBuckets(nowMs);
|
|
243
|
+
const bucket = authFailureBuckets.get(clientAddress);
|
|
244
|
+
if (bucket !== void 0 && bucket.resetAtMs > nowMs) {
|
|
245
|
+
bucket.failures += 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
authFailureBuckets.set(clientAddress, {
|
|
249
|
+
failures: 1,
|
|
250
|
+
resetAtMs: nowMs + authFailureLimit.windowMs
|
|
251
|
+
});
|
|
252
|
+
pruneAuthFailureBuckets(nowMs);
|
|
253
|
+
}
|
|
254
|
+
function clearAuthFailures(clientAddress) {
|
|
255
|
+
authFailureBuckets.delete(clientAddress);
|
|
256
|
+
}
|
|
257
|
+
async function closeActiveSession(sessionKey, closeOptions) {
|
|
258
|
+
const activeSession = activeSessions.get(sessionKey);
|
|
259
|
+
if (!activeSession) return;
|
|
260
|
+
activeSessions.delete(sessionKey);
|
|
261
|
+
try {
|
|
262
|
+
if (closeOptions.closeTransport) await activeSession.transport.close();
|
|
263
|
+
} finally {
|
|
264
|
+
await options.onSessionClosed?.(activeSession.identity);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function trackPendingNewSessionRequest() {
|
|
268
|
+
let resolvePending;
|
|
269
|
+
const pendingRequest = new Promise((resolve) => {
|
|
270
|
+
resolvePending = resolve;
|
|
271
|
+
});
|
|
272
|
+
pendingNewSessionRequests.add(pendingRequest);
|
|
273
|
+
return () => {
|
|
274
|
+
pendingNewSessionRequests.delete(pendingRequest);
|
|
275
|
+
resolvePending?.();
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async function createActiveSession(identityBase) {
|
|
279
|
+
const sessionId = randomUUID();
|
|
280
|
+
const sessionKey = activeSessionKey(identityBase.agentScopeId, sessionId);
|
|
281
|
+
let server = null;
|
|
282
|
+
const identity = createPortalAgentIdentity({
|
|
283
|
+
agentId: identityBase.agentId,
|
|
284
|
+
agentScopeId: identityBase.agentScopeId,
|
|
285
|
+
sessionId,
|
|
286
|
+
source: identityBase.source
|
|
287
|
+
});
|
|
288
|
+
const transport = new StreamableHTTPTransport({
|
|
289
|
+
onsessionclosed: () => {
|
|
290
|
+
closeActiveSession(sessionKey, { closeTransport: false }).catch((error) => reportSessionCloseError(error, identity));
|
|
291
|
+
},
|
|
292
|
+
onsessioninitialized: (initializedSessionId) => {
|
|
293
|
+
if (!server) throw new Error("MCP Portal session initialized before server connection.");
|
|
294
|
+
activeSessions.set(activeSessionKey(identityBase.agentScopeId, initializedSessionId), {
|
|
295
|
+
identity,
|
|
296
|
+
server,
|
|
297
|
+
transport
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
sessionIdGenerator: () => sessionId
|
|
301
|
+
});
|
|
302
|
+
server = createPortalMcpServer({
|
|
303
|
+
core: options.core,
|
|
304
|
+
scope: identity
|
|
305
|
+
});
|
|
306
|
+
await server.connect(transport);
|
|
307
|
+
return {
|
|
308
|
+
identity,
|
|
309
|
+
server,
|
|
310
|
+
transport
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async function closePortalSessions() {
|
|
314
|
+
closing = true;
|
|
315
|
+
const closeErrors = [];
|
|
316
|
+
while (activeSessions.size > 0 || pendingNewSessionRequests.size > 0) {
|
|
317
|
+
if (pendingNewSessionRequests.size > 0) {
|
|
318
|
+
await Promise.allSettled(pendingNewSessionRequests);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const closeResults = await Promise.allSettled([...activeSessions.keys()].map((sessionKey) => closeActiveSession(sessionKey, { closeTransport: true })));
|
|
322
|
+
closeErrors.push(...closeResults.filter((result) => result.status === "rejected").map((result) => errorFromUnknown(result.reason)));
|
|
323
|
+
}
|
|
324
|
+
if (closeErrors.length > 0) throw new AggregateError(closeErrors, "Failed to close one or more MCP Portal sessions.");
|
|
325
|
+
}
|
|
326
|
+
app.get("/health", (context) => context.json({
|
|
327
|
+
agents: [...options.registeredAgentIds ?? []].toSorted(),
|
|
328
|
+
ok: true
|
|
329
|
+
}));
|
|
330
|
+
app.all("/agents/:agentId/mcp", async (context) => {
|
|
331
|
+
const agentId = context.req.param("agentId");
|
|
332
|
+
const clientAddress = clientAddressFromContext(context);
|
|
333
|
+
if (closing) return unavailableResponse();
|
|
334
|
+
if (isAuthFailureRateLimited(clientAddress)) {
|
|
335
|
+
await auditAuth({
|
|
336
|
+
agentId,
|
|
337
|
+
clientAddress,
|
|
338
|
+
decision: "deny",
|
|
339
|
+
reason: "rate_limited"
|
|
340
|
+
});
|
|
341
|
+
return rateLimitedResponse();
|
|
342
|
+
}
|
|
343
|
+
const agentBearerAuth = options.agentBearerAuth;
|
|
344
|
+
const credentialVersion = agentBearerAuth.credentialVersionsByAgent?.[agentId];
|
|
345
|
+
const verification = verifyAgentBearerAuthorization({
|
|
346
|
+
agentId,
|
|
347
|
+
authorizationHeader: context.req.header(agentBearerAuth.authorizationHeaderName),
|
|
348
|
+
credentialVersion: credentialVersion ?? unknownAgentCredentialVersionForOpaqueAuthTiming,
|
|
349
|
+
masterKey: agentBearerAuth.masterKey
|
|
350
|
+
});
|
|
351
|
+
const agentIdentity = options.resolveAgentIdentity?.(agentId) ?? null;
|
|
352
|
+
if (agentIdentity === null) {
|
|
353
|
+
recordAuthFailure(clientAddress);
|
|
354
|
+
await auditAuth({
|
|
355
|
+
agentId,
|
|
356
|
+
clientAddress,
|
|
357
|
+
decision: "deny",
|
|
358
|
+
reason: "unknown_agent"
|
|
359
|
+
});
|
|
360
|
+
return unauthorizedResponse();
|
|
361
|
+
}
|
|
362
|
+
if (!verification.ok) {
|
|
363
|
+
recordAuthFailure(clientAddress);
|
|
364
|
+
await auditAuth({
|
|
365
|
+
agentId,
|
|
366
|
+
clientAddress,
|
|
367
|
+
decision: "deny",
|
|
368
|
+
reason: verification.reason
|
|
369
|
+
});
|
|
370
|
+
return unauthorizedResponse();
|
|
371
|
+
}
|
|
372
|
+
clearAuthFailures(clientAddress);
|
|
373
|
+
await auditAuth({
|
|
374
|
+
agentId,
|
|
375
|
+
clientAddress,
|
|
376
|
+
decision: "allow"
|
|
377
|
+
});
|
|
378
|
+
const mcpSessionId = context.req.header(mcpSessionIdHeader);
|
|
379
|
+
if (mcpSessionId) {
|
|
380
|
+
const activeSession = activeSessions.get(activeSessionKey(agentIdentity.agentScopeId, mcpSessionId));
|
|
381
|
+
if (!activeSession) return new Response("Unknown MCP portal session", { status: 404 });
|
|
382
|
+
return await activeSession.transport.handleRequest(context);
|
|
383
|
+
}
|
|
384
|
+
const finishPendingRequest = trackPendingNewSessionRequest();
|
|
385
|
+
try {
|
|
386
|
+
if (closing) return unavailableResponse();
|
|
387
|
+
const activeSession = await createActiveSession(agentIdentity);
|
|
388
|
+
if (closing) {
|
|
389
|
+
await activeSession.transport.close();
|
|
390
|
+
return unavailableResponse();
|
|
391
|
+
}
|
|
392
|
+
return await activeSession.transport.handleRequest(context);
|
|
393
|
+
} finally {
|
|
394
|
+
finishPendingRequest();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return Object.assign(app, { closePortalSessions });
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/mcp-proxy/resolve-agent-identity.ts
|
|
401
|
+
const approvalTokenMaxLifetimeMs = 5 * 6e4;
|
|
402
|
+
const approvalTokenReplayCacheLimit = 4096;
|
|
403
|
+
async function resolveAgentHmacKeyEntry(props) {
|
|
404
|
+
const envKey = props.envKeys.get(props.agentId);
|
|
405
|
+
if (envKey !== void 0) return [props.agentId, envKey];
|
|
406
|
+
if (props.agent.hmacKey === void 0) throw new Error(`Missing HMAC key for MCP Portal agent "${props.agentId}".`);
|
|
407
|
+
const secretValue = await props.resolveSecret(props.agent.hmacKey);
|
|
408
|
+
if (!/^[0-9a-f]+$/u.test(secretValue) || secretValue.length !== 64) throw new Error(`MCP Portal agent "${props.agentId}" HMAC key must be 64 hex characters.`);
|
|
409
|
+
return [props.agentId, Buffer.from(secretValue, "hex")];
|
|
410
|
+
}
|
|
411
|
+
async function resolveAgentHmacKeys(props) {
|
|
412
|
+
return new Map(await Promise.all(Object.entries(props.agents).map(([agentId, agent]) => resolveAgentHmacKeyEntry({
|
|
413
|
+
agent,
|
|
414
|
+
agentId,
|
|
415
|
+
envKeys: props.envKeys,
|
|
416
|
+
resolveSecret: props.resolveSecret
|
|
417
|
+
}))));
|
|
418
|
+
}
|
|
419
|
+
function createPortalAgentRuntimeRecords(props) {
|
|
420
|
+
const records = /* @__PURE__ */ new Map();
|
|
421
|
+
for (const [agentId, agent] of Object.entries(props.portalConfig.agents)) {
|
|
422
|
+
const hmacKey = props.hmacKeys.get(agentId);
|
|
423
|
+
if (hmacKey === void 0) throw new Error(`Missing HMAC key for MCP Portal agent "${agentId}".`);
|
|
424
|
+
records.set(agentId, {
|
|
425
|
+
agentId,
|
|
426
|
+
hmacKey,
|
|
427
|
+
profile: resolveMcpPortalProfile(props.portalConfig, agent.profile),
|
|
428
|
+
profileName: agent.profile
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return records;
|
|
432
|
+
}
|
|
433
|
+
function createPortalHttpAgentResolver(records) {
|
|
434
|
+
return (agentId) => {
|
|
435
|
+
if (records.get(agentId) === void 0) return null;
|
|
436
|
+
return createPortalAgentIdentity({
|
|
437
|
+
agentId,
|
|
438
|
+
agentScopeId: agentId,
|
|
439
|
+
source: "mcp-proxy-bearer"
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function approvalRequiredByProfile(profile, call) {
|
|
444
|
+
const annotations = call.tool.annotations;
|
|
445
|
+
return mcpPortalCallRequiresApproval(profile, {
|
|
446
|
+
...annotations === void 0 ? {} : { annotations },
|
|
447
|
+
namespace: call.namespace,
|
|
448
|
+
toolName: call.toolName
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function approvalTokenCallDigests(calls) {
|
|
452
|
+
return calls.map((call) => ({
|
|
453
|
+
argumentsHash: hashCallArguments(call.arguments),
|
|
454
|
+
namespace: call.namespace,
|
|
455
|
+
toolName: call.toolName
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
function createPortalApprovalVerifier(props) {
|
|
459
|
+
function auditApproval(event) {
|
|
460
|
+
const auditEvent = {
|
|
461
|
+
...event,
|
|
462
|
+
kind: "mcp_portal_approval",
|
|
463
|
+
timeMs: Date.now()
|
|
464
|
+
};
|
|
465
|
+
try {
|
|
466
|
+
props.auditSink?.(auditEvent);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
props.auditErrorSink?.(error instanceof Error ? error : new Error(String(error)), auditEvent);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const consumedApprovalTokenIds = /* @__PURE__ */ new Map();
|
|
472
|
+
const replayCacheLimit = props.approvalTokenReplayCacheLimit ?? approvalTokenReplayCacheLimit;
|
|
473
|
+
const consumeTokenId = (agentId, jti, expiresAtMs) => {
|
|
474
|
+
const nowMs = Date.now();
|
|
475
|
+
for (const [tokenKey, tokenExpiresAtMs] of consumedApprovalTokenIds) if (tokenExpiresAtMs <= nowMs) consumedApprovalTokenIds.delete(tokenKey);
|
|
476
|
+
const tokenKey = `${agentId}\n${jti}`;
|
|
477
|
+
if (consumedApprovalTokenIds.has(tokenKey)) return {
|
|
478
|
+
ok: false,
|
|
479
|
+
reason: "replayed"
|
|
480
|
+
};
|
|
481
|
+
if (consumedApprovalTokenIds.size >= replayCacheLimit) return {
|
|
482
|
+
ok: false,
|
|
483
|
+
reason: "replay-cache-full"
|
|
484
|
+
};
|
|
485
|
+
consumedApprovalTokenIds.set(tokenKey, expiresAtMs);
|
|
486
|
+
return { ok: true };
|
|
487
|
+
};
|
|
488
|
+
return (calls, agentId, token) => {
|
|
489
|
+
const record = props.records.get(agentId);
|
|
490
|
+
if (record === void 0) {
|
|
491
|
+
auditApproval({
|
|
492
|
+
agentId,
|
|
493
|
+
decision: "deny",
|
|
494
|
+
reason: "approval_token_invalid",
|
|
495
|
+
verifierReason: "unknown-agent"
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
kind: "approval_token_invalid",
|
|
499
|
+
reason: "unknown-agent"
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const callsRequiringApproval = calls.filter((call) => approvalRequiredByProfile(record.profile, call));
|
|
503
|
+
if (callsRequiringApproval.length === 0) {
|
|
504
|
+
auditApproval({
|
|
505
|
+
agentId,
|
|
506
|
+
decision: "allow",
|
|
507
|
+
reason: "no_approval_required"
|
|
508
|
+
});
|
|
509
|
+
return { kind: "allow" };
|
|
510
|
+
}
|
|
511
|
+
if (token === void 0) {
|
|
512
|
+
auditApproval({
|
|
513
|
+
agentId,
|
|
514
|
+
decision: "deny",
|
|
515
|
+
reason: "approval_token_missing"
|
|
516
|
+
});
|
|
517
|
+
return { kind: "approval_token_missing" };
|
|
518
|
+
}
|
|
519
|
+
const verification = verifyApprovalToken({
|
|
520
|
+
agentId,
|
|
521
|
+
calls: approvalTokenCallDigests(callsRequiringApproval),
|
|
522
|
+
consumeTokenId: (jti, expiresAtMs) => consumeTokenId(agentId, jti, expiresAtMs),
|
|
523
|
+
key: record.hmacKey,
|
|
524
|
+
maxLifetimeMs: approvalTokenMaxLifetimeMs,
|
|
525
|
+
nowMs: Date.now(),
|
|
526
|
+
token
|
|
527
|
+
});
|
|
528
|
+
if (verification.ok) {
|
|
529
|
+
auditApproval({
|
|
530
|
+
agentId,
|
|
531
|
+
decision: "allow"
|
|
532
|
+
});
|
|
533
|
+
return { kind: "allow" };
|
|
534
|
+
}
|
|
535
|
+
auditApproval({
|
|
536
|
+
agentId,
|
|
537
|
+
decision: "deny",
|
|
538
|
+
reason: "approval_token_invalid",
|
|
539
|
+
verifierReason: verification.reason
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
kind: "approval_token_invalid",
|
|
543
|
+
reason: verification.reason
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
export { createPortalHttpApp as a, listPortalMcpTools as c, resolveAgentHmacKeys as i, portalMcpToolNames as l, createPortalApprovalVerifier as n, createPortalMcpServer as o, createPortalHttpAgentResolver as r, emitMcpProgress as s, createPortalAgentRuntimeRecords as t };
|
|
549
|
+
|
|
550
|
+
//# sourceMappingURL=resolve-agent-identity-DnC_Pmnh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-agent-identity-DnC_Pmnh.js","names":[],"sources":["../src/mcp-proxy/portal-mcp-server.ts","../src/mcp-proxy/portal-http-server.ts","../src/mcp-proxy/resolve-agent-identity.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport {\n\tCallToolRequestSchema,\n\tListToolsRequestSchema,\n\ttype CallToolResult,\n\ttype Tool,\n} from '@modelcontextprotocol/sdk/types.js';\n\nimport type {\n\tPortalAgentScope,\n\tPortalCore,\n\tPortalCoreEvent,\n\tPortalCoreToolDescriptor,\n\tPortalCoreResult,\n\tPortalCoreToolName,\n} from '../core/portal-core.js';\nimport { portalToolInputSchemas } from '../core/portal-tools.js';\nimport { redactThrownError } from '../upstream-response-middleware.js';\n\nexport const portalMcpToolNames = [\n\t'mcp_portal_list',\n\t'mcp_portal_search',\n\t'mcp_portal_describe',\n\t'mcp_portal_call',\n] as const;\n\nexport type PortalMcpToolName = (typeof portalMcpToolNames)[number];\n\nconst portalMcpToolNameSet = new Set<string>(portalMcpToolNames);\n\nfunction isPortalCoreToolName(value: string): value is PortalCoreToolName {\n\treturn portalMcpToolNameSet.has(value);\n}\n\nexport function listPortalMcpTools(\n\tdescriptors?: readonly PortalCoreToolDescriptor[],\n): readonly Tool[] {\n\treturn (\n\t\tdescriptors ?? [\n\t\t\t{\n\t\t\t\tdescription: 'List authorized MCP namespaces and compact tool summaries.',\n\t\t\t\tinputSchema: portalToolInputSchemas.mcp_portal_list,\n\t\t\t\tname: 'mcp_portal_list',\n\t\t\t},\n\t\t\t{\n\t\t\t\tdescription: 'Search the caller scoped MCP Portal index.',\n\t\t\t\tinputSchema: portalToolInputSchemas.mcp_portal_search,\n\t\t\t\tname: 'mcp_portal_search',\n\t\t\t},\n\t\t\t{\n\t\t\t\tdescription: 'Describe exact MCP tool schemas and optional TypeScript/Zod helpers.',\n\t\t\t\tinputSchema: portalToolInputSchemas.mcp_portal_describe,\n\t\t\t\tname: 'mcp_portal_describe',\n\t\t\t},\n\t\t\t{\n\t\t\t\tdescription: 'Validate and call an authorized upstream MCP tool by namespace and toolName.',\n\t\t\t\tinputSchema: portalToolInputSchemas.mcp_portal_call,\n\t\t\t\tname: 'mcp_portal_call',\n\t\t\t},\n\t\t]\n\t);\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction coreResultIsError(value: PortalCoreResult): boolean {\n\tif (value.items.some((item) => item.status === 'failed')) {\n\t\treturn true;\n\t}\n\tif (!isObjectRecord(value.structuredContent)) {\n\t\treturn false;\n\t}\n\treturn value.structuredContent.ok === false;\n}\n\nfunction jsonToolResult(value: PortalCoreResult): CallToolResult {\n\treturn {\n\t\tcontent: [{ text: JSON.stringify(value), type: 'text' }],\n\t\t...(coreResultIsError(value) ? { isError: true } : {}),\n\t};\n}\n\nfunction errorToolResult(error: unknown): CallToolResult {\n\tconst redactedError = redactThrownError(error);\n\treturn {\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttext: redactedError.message,\n\t\t\t\ttype: 'text',\n\t\t\t},\n\t\t],\n\t\tisError: true,\n\t};\n}\n\nasync function sendBestEffortNotification(\n\tnotification: {\n\t\treadonly method: 'notifications/message' | 'notifications/progress';\n\t\treadonly params: Record<string, unknown>;\n\t},\n\tsendNotification: (notification: {\n\t\treadonly method: 'notifications/message' | 'notifications/progress';\n\t\treadonly params: Record<string, unknown>;\n\t}) => Promise<void>,\n): Promise<void> {\n\ttry {\n\t\tawait sendNotification(notification);\n\t} catch {\n\t\t// Progress is advisory; unsupported client notification channels must not fail the tool call.\n\t}\n}\n\nexport async function emitMcpProgress(props: {\n\treadonly event: PortalCoreEvent;\n\treadonly sendNotification: (notification: {\n\t\treadonly method: 'notifications/message' | 'notifications/progress';\n\t\treadonly params: Record<string, unknown>;\n\t}) => Promise<void>;\n\treadonly progressToken: number | string | undefined;\n}): Promise<void> {\n\tif (props.event.kind === 'upstream_notification') {\n\t\tif (!isObjectRecord(props.event.params)) {\n\t\t\treturn;\n\t\t}\n\t\tif (props.event.method === 'notifications/progress') {\n\t\t\tif (props.progressToken === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait sendBestEffortNotification(\n\t\t\t\t{\n\t\t\t\t\tmethod: 'notifications/progress',\n\t\t\t\t\tparams: { ...props.event.params, progressToken: props.progressToken },\n\t\t\t\t},\n\t\t\t\tprops.sendNotification,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (props.event.method !== 'notifications/message') {\n\t\t\treturn;\n\t\t}\n\t\tawait sendBestEffortNotification(\n\t\t\t{\n\t\t\t\tmethod: 'notifications/message',\n\t\t\t\tparams: props.event.params,\n\t\t\t},\n\t\t\tprops.sendNotification,\n\t\t);\n\t\treturn;\n\t}\n\tif (props.event.kind !== 'progress' && props.event.kind !== 'partial_content') {\n\t\treturn;\n\t}\n\tconst message =\n\t\tprops.event.kind === 'progress'\n\t\t\t? props.event.message\n\t\t\t: props.event.content.type === 'text'\n\t\t\t\t? props.event.content.text\n\t\t\t\t: JSON.stringify(props.event.content.value);\n\tif (message === undefined || message.length === 0) {\n\t\treturn;\n\t}\n\tif (props.progressToken !== undefined) {\n\t\tawait sendBestEffortNotification(\n\t\t\t{\n\t\t\t\tmethod: 'notifications/progress',\n\t\t\t\tparams: {\n\t\t\t\t\tmessage,\n\t\t\t\t\tprogress: props.event.kind === 'progress' ? (props.event.progress ?? 0) : 0,\n\t\t\t\t\tprogressToken: props.progressToken,\n\t\t\t\t\t...(props.event.kind === 'progress' && props.event.total !== undefined\n\t\t\t\t\t\t? { total: props.event.total }\n\t\t\t\t\t\t: {}),\n\t\t\t\t},\n\t\t\t},\n\t\t\tprops.sendNotification,\n\t\t);\n\t\treturn;\n\t}\n\tawait sendBestEffortNotification(\n\t\t{\n\t\t\tmethod: 'notifications/message',\n\t\t\tparams: {\n\t\t\t\tdata: message,\n\t\t\t\tlevel: 'info',\n\t\t\t},\n\t\t},\n\t\tprops.sendNotification,\n\t);\n}\n\nexport function createPortalMcpServer(props: {\n\treadonly core: PortalCore;\n\treadonly scope: PortalAgentScope;\n}): Server {\n\tconst server = new Server(\n\t\t{ name: 'mcp-portal', version: '1.0.0' },\n\t\t{ capabilities: { tools: { listChanged: false } } },\n\t);\n\n\tserver.setRequestHandler(ListToolsRequestSchema, async () => ({\n\t\ttools: listPortalMcpTools(props.core.describeTools(props.scope)),\n\t}));\n\n\tserver.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n\t\tif (!isPortalCoreToolName(request.params.name)) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ text: `Unknown MCP Portal tool: ${request.params.name}`, type: 'text' }],\n\t\t\t\tisError: true,\n\t\t\t};\n\t\t}\n\t\ttry {\n\t\t\tconst result = await props.core.collectPortalCoreResult(\n\t\t\t\tprops.core.callStream({\n\t\t\t\t\tinput: request.params.arguments ?? {},\n\t\t\t\t\tscope: props.scope,\n\t\t\t\t\tsignal: extra.signal,\n\t\t\t\t\ttoolName: request.params.name,\n\t\t\t\t}),\n\t\t\t\t{\n\t\t\t\t\tonEvent: async (event) => {\n\t\t\t\t\t\tawait emitMcpProgress({\n\t\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\tprogressToken: extra['_meta']?.progressToken,\n\t\t\t\t\t\t\tsendNotification: extra.sendNotification,\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn jsonToolResult(result);\n\t\t} catch (error) {\n\t\t\treturn errorToolResult(error);\n\t\t}\n\t});\n\n\treturn server;\n}\n","import { randomUUID } from 'node:crypto';\n\nimport { StreamableHTTPTransport } from '@hono/mcp';\nimport { getConnInfo } from '@hono/node-server/conninfo';\nimport { Hono, type Context } from 'hono';\n\nimport type { PortalCore } from '../core/portal-core.js';\nimport { createPortalAgentIdentity, type PortalAgentIdentity } from '../portal-access-policy.js';\nimport { verifyAgentBearerAuthorization } from '../portal-auth/agent-bearer-token.js';\nimport { createPortalMcpServer } from './portal-mcp-server.js';\n\nexport interface PortalHttpAgentIdentity extends PortalAgentIdentity {}\n\nexport interface PortalAgentBearerAuth {\n\treadonly authorizationHeaderName: string;\n\treadonly credentialVersionsByAgent?: Readonly<Record<string, number>>;\n\treadonly masterKey: Buffer;\n}\n\nexport type PortalHttpAuditEvent = {\n\treadonly agentId: string;\n\treadonly clientAddress: string;\n\treadonly decision: 'allow' | 'deny';\n\treadonly kind: 'mcp_proxy_auth';\n\treadonly reason?:\n\t\t| 'malformed'\n\t\t| 'missing'\n\t\t| 'rate_limited'\n\t\t| 'signature-mismatch'\n\t\t| 'unknown_agent';\n\treadonly timeMs: number;\n};\n\nexport interface PortalHttpAppOptions {\n\treadonly agentBearerAuth: PortalAgentBearerAuth;\n\treadonly auditErrorSink?: (error: Error, event: PortalHttpAuditEvent) => Promise<void> | void;\n\treadonly auditSink?: (event: PortalHttpAuditEvent) => Promise<void> | void;\n\treadonly authFailureLimit?: {\n\t\treadonly maxFailures: number;\n\t\treadonly windowMs: number;\n\t};\n\treadonly core: PortalCore;\n\treadonly onSessionClosed?: (identity: PortalAgentIdentity) => Promise<void> | void;\n\treadonly onSessionCloseError?: (\n\t\terror: Error,\n\t\tidentity: PortalAgentIdentity,\n\t) => Promise<void> | void;\n\treadonly registeredAgentIds?: readonly string[];\n\treadonly resolveAgentIdentity?: (agentId: string) => PortalHttpAgentIdentity | null;\n}\n\nexport type PortalHttpApp = Hono & {\n\treadonly closePortalSessions: () => Promise<void>;\n};\n\nconst mcpSessionIdHeader = 'mcp-session-id';\nconst defaultAuthFailureLimit = { maxFailures: 60, windowMs: 60_000 } as const;\nconst authFailureBucketLimit = 1_024;\nconst directClientAddress = 'direct-client';\nconst unknownAgentCredentialVersionForOpaqueAuthTiming = 1;\n\ninterface ActivePortalMcpSession {\n\treadonly identity: PortalAgentIdentity;\n\treadonly server: ReturnType<typeof createPortalMcpServer>;\n\treadonly transport: StreamableHTTPTransport;\n}\n\ninterface AuthFailureBucket {\n\treadonly resetAtMs: number;\n\tfailures: number;\n}\n\nfunction activeSessionKey(scopeId: string, sessionId: string): string {\n\treturn `${scopeId}\\n${sessionId}`;\n}\n\nfunction unauthorizedResponse(): Response {\n\treturn Response.json({ error: { kind: 'unauthorized' }, ok: false }, { status: 401 });\n}\n\nfunction rateLimitedResponse(): Response {\n\treturn Response.json({ error: { kind: 'rate_limited' }, ok: false }, { status: 429 });\n}\n\nfunction unavailableResponse(): Response {\n\treturn Response.json({ error: { kind: 'shutting_down' }, ok: false }, { status: 503 });\n}\n\nfunction errorFromUnknown(error: unknown): Error {\n\treturn error instanceof Error ? error : new Error(String(error));\n}\n\nfunction clientAddressFromContext(context: Context): string {\n\ttry {\n\t\tconst address = getConnInfo(context).remote.address;\n\t\treturn address && address.length > 0 ? address : directClientAddress;\n\t} catch {\n\t\treturn directClientAddress;\n\t}\n}\n\nexport function createPortalHttpApp(options: PortalHttpAppOptions): PortalHttpApp {\n\tif (options.agentBearerAuth === undefined) {\n\t\tthrow new Error('MCP Portal HTTP app requires agent bearer auth.');\n\t}\n\tif (options.agentBearerAuth.authorizationHeaderName.length === 0) {\n\t\tthrow new Error('MCP Portal HTTP app requires agent bearer auth header name.');\n\t}\n\tconst app = new Hono();\n\tconst activeSessions = new Map<string, ActivePortalMcpSession>();\n\tconst authFailureBuckets = new Map<string, AuthFailureBucket>();\n\tconst pendingNewSessionRequests = new Set<Promise<void>>();\n\tconst authFailureLimit = options.authFailureLimit ?? defaultAuthFailureLimit;\n\tlet closing = false;\n\n\tasync function auditAuth(event: Omit<PortalHttpAuditEvent, 'kind' | 'timeMs'>): Promise<void> {\n\t\tconst auditEvent = { ...event, kind: 'mcp_proxy_auth', timeMs: Date.now() } as const;\n\t\ttry {\n\t\t\tawait options.auditSink?.(auditEvent);\n\t\t} catch (error) {\n\t\t\tawait options.auditErrorSink?.(errorFromUnknown(error), auditEvent);\n\t\t}\n\t}\n\n\tasync function reportSessionCloseError(\n\t\terror: unknown,\n\t\tidentity: PortalAgentIdentity,\n\t): Promise<void> {\n\t\tawait options.onSessionCloseError?.(errorFromUnknown(error), identity);\n\t}\n\n\tfunction pruneAuthFailureBuckets(nowMs: number): void {\n\t\tfor (const [key, bucket] of authFailureBuckets) {\n\t\t\tif (bucket.resetAtMs <= nowMs) {\n\t\t\t\tauthFailureBuckets.delete(key);\n\t\t\t}\n\t\t}\n\t\twhile (authFailureBuckets.size > authFailureBucketLimit) {\n\t\t\tconst firstKey = authFailureBuckets.keys().next().value;\n\t\t\tif (typeof firstKey !== 'string') {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauthFailureBuckets.delete(firstKey);\n\t\t}\n\t}\n\n\tfunction isAuthFailureRateLimited(clientAddress: string): boolean {\n\t\tconst nowMs = Date.now();\n\t\tpruneAuthFailureBuckets(nowMs);\n\t\tconst bucket = authFailureBuckets.get(clientAddress);\n\t\tif (bucket === undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (bucket.resetAtMs <= nowMs) {\n\t\t\tauthFailureBuckets.delete(clientAddress);\n\t\t\treturn false;\n\t\t}\n\t\treturn bucket.failures >= authFailureLimit.maxFailures;\n\t}\n\n\tfunction recordAuthFailure(clientAddress: string): void {\n\t\tconst nowMs = Date.now();\n\t\tpruneAuthFailureBuckets(nowMs);\n\t\tconst bucket = authFailureBuckets.get(clientAddress);\n\t\tif (bucket !== undefined && bucket.resetAtMs > nowMs) {\n\t\t\tbucket.failures += 1;\n\t\t\treturn;\n\t\t}\n\t\tauthFailureBuckets.set(clientAddress, {\n\t\t\tfailures: 1,\n\t\t\tresetAtMs: nowMs + authFailureLimit.windowMs,\n\t\t});\n\t\tpruneAuthFailureBuckets(nowMs);\n\t}\n\n\tfunction clearAuthFailures(clientAddress: string): void {\n\t\tauthFailureBuckets.delete(clientAddress);\n\t}\n\n\tasync function closeActiveSession(\n\t\tsessionKey: string,\n\t\tcloseOptions: { readonly closeTransport: boolean },\n\t): Promise<void> {\n\t\tconst activeSession = activeSessions.get(sessionKey);\n\t\tif (!activeSession) {\n\t\t\treturn;\n\t\t}\n\t\tactiveSessions.delete(sessionKey);\n\t\ttry {\n\t\t\tif (closeOptions.closeTransport) {\n\t\t\t\tawait activeSession.transport.close();\n\t\t\t}\n\t\t} finally {\n\t\t\tawait options.onSessionClosed?.(activeSession.identity);\n\t\t}\n\t}\n\n\tfunction trackPendingNewSessionRequest(): () => void {\n\t\tlet resolvePending: (() => void) | undefined;\n\t\tconst pendingRequest = new Promise<void>((resolve) => {\n\t\t\tresolvePending = resolve;\n\t\t});\n\t\tpendingNewSessionRequests.add(pendingRequest);\n\t\treturn () => {\n\t\t\tpendingNewSessionRequests.delete(pendingRequest);\n\t\t\tresolvePending?.();\n\t\t};\n\t}\n\n\tasync function createActiveSession(\n\t\tidentityBase: PortalAgentIdentity,\n\t): Promise<ActivePortalMcpSession> {\n\t\tconst sessionId = randomUUID();\n\t\tconst sessionKey = activeSessionKey(identityBase.agentScopeId, sessionId);\n\t\tlet server: ReturnType<typeof createPortalMcpServer> | null = null;\n\t\tconst identity = createPortalAgentIdentity({\n\t\t\tagentId: identityBase.agentId,\n\t\t\tagentScopeId: identityBase.agentScopeId,\n\t\t\tsessionId,\n\t\t\tsource: identityBase.source,\n\t\t});\n\t\tconst transport = new StreamableHTTPTransport({\n\t\t\tonsessionclosed: () => {\n\t\t\t\tvoid closeActiveSession(sessionKey, { closeTransport: false }).catch((error: unknown) =>\n\t\t\t\t\treportSessionCloseError(error, identity),\n\t\t\t\t);\n\t\t\t},\n\t\t\tonsessioninitialized: (initializedSessionId) => {\n\t\t\t\tif (!server) {\n\t\t\t\t\tthrow new Error('MCP Portal session initialized before server connection.');\n\t\t\t\t}\n\t\t\t\tactiveSessions.set(activeSessionKey(identityBase.agentScopeId, initializedSessionId), {\n\t\t\t\t\tidentity,\n\t\t\t\t\tserver,\n\t\t\t\t\ttransport,\n\t\t\t\t});\n\t\t\t},\n\t\t\tsessionIdGenerator: () => sessionId,\n\t\t});\n\t\tserver = createPortalMcpServer({\n\t\t\tcore: options.core,\n\t\t\tscope: identity,\n\t\t});\n\t\tawait server.connect(transport);\n\t\treturn { identity, server, transport };\n\t}\n\n\tasync function closePortalSessions(): Promise<void> {\n\t\tclosing = true;\n\t\tconst closeErrors: Error[] = [];\n\t\twhile (activeSessions.size > 0 || pendingNewSessionRequests.size > 0) {\n\t\t\tif (pendingNewSessionRequests.size > 0) {\n\t\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\t\tawait Promise.allSettled(pendingNewSessionRequests);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Closing may trigger callbacks that mutate activeSessions, so drain snapshots until empty.\n\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\tconst closeResults = await Promise.allSettled(\n\t\t\t\t[...activeSessions.keys()].map((sessionKey) =>\n\t\t\t\t\tcloseActiveSession(sessionKey, { closeTransport: true }),\n\t\t\t\t),\n\t\t\t);\n\t\t\tcloseErrors.push(\n\t\t\t\t...closeResults\n\t\t\t\t\t.filter((result): result is PromiseRejectedResult => result.status === 'rejected')\n\t\t\t\t\t.map((result): Error => errorFromUnknown(result.reason)),\n\t\t\t);\n\t\t}\n\t\tif (closeErrors.length > 0) {\n\t\t\tthrow new AggregateError(closeErrors, 'Failed to close one or more MCP Portal sessions.');\n\t\t}\n\t}\n\n\tapp.get('/health', (context) =>\n\t\tcontext.json({ agents: [...(options.registeredAgentIds ?? [])].toSorted(), ok: true }),\n\t);\n\n\tapp.all('/agents/:agentId/mcp', async (context) => {\n\t\tconst agentId = context.req.param('agentId');\n\t\tconst clientAddress = clientAddressFromContext(context);\n\t\tif (closing) {\n\t\t\treturn unavailableResponse();\n\t\t}\n\t\tif (isAuthFailureRateLimited(clientAddress)) {\n\t\t\tawait auditAuth({\n\t\t\t\tagentId,\n\t\t\t\tclientAddress,\n\t\t\t\tdecision: 'deny',\n\t\t\t\treason: 'rate_limited',\n\t\t\t});\n\t\t\treturn rateLimitedResponse();\n\t\t}\n\t\tconst agentBearerAuth = options.agentBearerAuth;\n\t\tconst credentialVersion = agentBearerAuth.credentialVersionsByAgent?.[agentId];\n\t\tconst verification = verifyAgentBearerAuthorization({\n\t\t\tagentId,\n\t\t\tauthorizationHeader: context.req.header(agentBearerAuth.authorizationHeaderName),\n\t\t\tcredentialVersion: credentialVersion ?? unknownAgentCredentialVersionForOpaqueAuthTiming,\n\t\t\tmasterKey: agentBearerAuth.masterKey,\n\t\t});\n\t\tconst agentIdentity = options.resolveAgentIdentity?.(agentId) ?? null;\n\t\tif (agentIdentity === null) {\n\t\t\trecordAuthFailure(clientAddress);\n\t\t\tawait auditAuth({\n\t\t\t\tagentId,\n\t\t\t\tclientAddress,\n\t\t\t\tdecision: 'deny',\n\t\t\t\treason: 'unknown_agent',\n\t\t\t});\n\t\t\treturn unauthorizedResponse();\n\t\t}\n\t\tif (!verification.ok) {\n\t\t\trecordAuthFailure(clientAddress);\n\t\t\tawait auditAuth({\n\t\t\t\tagentId,\n\t\t\t\tclientAddress,\n\t\t\t\tdecision: 'deny',\n\t\t\t\treason: verification.reason,\n\t\t\t});\n\t\t\treturn unauthorizedResponse();\n\t\t}\n\t\tclearAuthFailures(clientAddress);\n\t\tawait auditAuth({ agentId, clientAddress, decision: 'allow' });\n\n\t\tconst mcpSessionId = context.req.header(mcpSessionIdHeader);\n\t\tif (mcpSessionId) {\n\t\t\tconst activeSession = activeSessions.get(\n\t\t\t\tactiveSessionKey(agentIdentity.agentScopeId, mcpSessionId),\n\t\t\t);\n\t\t\tif (!activeSession) {\n\t\t\t\treturn new Response('Unknown MCP portal session', { status: 404 });\n\t\t\t}\n\t\t\treturn await activeSession.transport.handleRequest(context);\n\t\t}\n\n\t\tconst finishPendingRequest = trackPendingNewSessionRequest();\n\t\ttry {\n\t\t\tif (closing) {\n\t\t\t\treturn unavailableResponse();\n\t\t\t}\n\t\t\tconst activeSession = await createActiveSession(agentIdentity);\n\t\t\tif (closing) {\n\t\t\t\tawait activeSession.transport.close();\n\t\t\t\treturn unavailableResponse();\n\t\t\t}\n\t\t\treturn await activeSession.transport.handleRequest(context);\n\t\t} finally {\n\t\t\tfinishPendingRequest();\n\t\t}\n\t});\n\n\treturn Object.assign(app, { closePortalSessions });\n}\n","import {\n\tresolveMcpPortalProfile,\n\tmcpPortalCallRequiresApproval,\n\ttype McpPortalAgentConfig,\n\ttype McpPortalConfig,\n\ttype ResolvedMcpPortalProfile,\n\ttype SecretValue,\n} from '@agent-vm/config-contracts';\n\nimport type { PortalApprovalCall } from '../core/portal-tools.js';\nimport { createPortalAgentIdentity } from '../portal-access-policy.js';\nimport { hashCallArguments, verifyApprovalToken } from '../portal-auth/hmac-token.js';\n\nconst approvalTokenMaxLifetimeMs = 5 * 60_000;\nconst approvalTokenReplayCacheLimit = 4_096;\n\nexport interface ResolveAgentHmacKeysProps {\n\treadonly agents: Readonly<Record<string, McpPortalAgentConfig>>;\n\treadonly envKeys: ReadonlyMap<string, Buffer>;\n\treadonly resolveSecret: (secret: SecretValue) => Promise<string>;\n}\n\nexport interface PortalAgentRuntimeRecord {\n\treadonly agentId: string;\n\treadonly hmacKey: Buffer;\n\treadonly profile: ResolvedMcpPortalProfile;\n\treadonly profileName: string;\n}\n\nexport interface PortalApprovalAuditEvent {\n\treadonly agentId: string;\n\treadonly decision: 'allow' | 'deny';\n\treadonly kind: 'mcp_portal_approval';\n\treadonly reason?: 'approval_token_invalid' | 'approval_token_missing' | 'no_approval_required';\n\treadonly timeMs: number;\n\treadonly verifierReason?: string;\n}\n\nasync function resolveAgentHmacKeyEntry(props: {\n\treadonly agent: McpPortalAgentConfig;\n\treadonly agentId: string;\n\treadonly envKeys: ReadonlyMap<string, Buffer>;\n\treadonly resolveSecret: (secret: SecretValue) => Promise<string>;\n}): Promise<readonly [string, Buffer]> {\n\tconst envKey = props.envKeys.get(props.agentId);\n\tif (envKey !== undefined) {\n\t\treturn [props.agentId, envKey];\n\t}\n\tif (props.agent.hmacKey === undefined) {\n\t\tthrow new Error(`Missing HMAC key for MCP Portal agent \"${props.agentId}\".`);\n\t}\n\tconst secretValue = await props.resolveSecret(props.agent.hmacKey);\n\tif (!/^[0-9a-f]+$/u.test(secretValue) || secretValue.length !== 64) {\n\t\tthrow new Error(`MCP Portal agent \"${props.agentId}\" HMAC key must be 64 hex characters.`);\n\t}\n\treturn [props.agentId, Buffer.from(secretValue, 'hex')];\n}\n\nexport async function resolveAgentHmacKeys(\n\tprops: ResolveAgentHmacKeysProps,\n): Promise<ReadonlyMap<string, Buffer>> {\n\treturn new Map(\n\t\tawait Promise.all(\n\t\t\tObject.entries(props.agents).map(([agentId, agent]) =>\n\t\t\t\tresolveAgentHmacKeyEntry({\n\t\t\t\t\tagent,\n\t\t\t\t\tagentId,\n\t\t\t\t\tenvKeys: props.envKeys,\n\t\t\t\t\tresolveSecret: props.resolveSecret,\n\t\t\t\t}),\n\t\t\t),\n\t\t),\n\t);\n}\n\nexport function createPortalAgentRuntimeRecords(props: {\n\treadonly hmacKeys: ReadonlyMap<string, Buffer>;\n\treadonly portalConfig: McpPortalConfig;\n}): ReadonlyMap<string, PortalAgentRuntimeRecord> {\n\tconst records = new Map<string, PortalAgentRuntimeRecord>();\n\tfor (const [agentId, agent] of Object.entries(props.portalConfig.agents)) {\n\t\tconst hmacKey = props.hmacKeys.get(agentId);\n\t\tif (hmacKey === undefined) {\n\t\t\tthrow new Error(`Missing HMAC key for MCP Portal agent \"${agentId}\".`);\n\t\t}\n\t\trecords.set(agentId, {\n\t\t\tagentId,\n\t\t\thmacKey,\n\t\t\tprofile: resolveMcpPortalProfile(props.portalConfig, agent.profile),\n\t\t\tprofileName: agent.profile,\n\t\t});\n\t}\n\treturn records;\n}\n\nexport function createPortalHttpAgentResolver(\n\trecords: ReadonlyMap<string, PortalAgentRuntimeRecord>,\n): (agentId: string) => ReturnType<typeof createPortalAgentIdentity> | null {\n\treturn (agentId) => {\n\t\tconst record = records.get(agentId);\n\t\tif (record === undefined) {\n\t\t\treturn null;\n\t\t}\n\t\treturn createPortalAgentIdentity({\n\t\t\tagentId,\n\t\t\tagentScopeId: agentId,\n\t\t\tsource: 'mcp-proxy-bearer',\n\t\t});\n\t};\n}\n\nfunction approvalRequiredByProfile(\n\tprofile: ResolvedMcpPortalProfile,\n\tcall: PortalApprovalCall,\n): boolean {\n\tconst annotations = call.tool.annotations;\n\treturn mcpPortalCallRequiresApproval(profile, {\n\t\t...(annotations === undefined ? {} : { annotations }),\n\t\tnamespace: call.namespace,\n\t\ttoolName: call.toolName,\n\t});\n}\n\nfunction approvalTokenCallDigests(calls: readonly PortalApprovalCall[]): readonly {\n\treadonly argumentsHash: string;\n\treadonly namespace: string;\n\treadonly toolName: string;\n}[] {\n\treturn calls.map((call) => ({\n\t\targumentsHash: hashCallArguments(call.arguments),\n\t\tnamespace: call.namespace,\n\t\ttoolName: call.toolName,\n\t}));\n}\n\nexport function createPortalApprovalVerifier(props: {\n\treadonly approvalTokenReplayCacheLimit?: number;\n\treadonly auditErrorSink?: (error: Error, event: PortalApprovalAuditEvent) => void;\n\treadonly auditSink?: (event: PortalApprovalAuditEvent) => void;\n\treadonly records: ReadonlyMap<string, PortalAgentRuntimeRecord>;\n}): (\n\tcalls: readonly PortalApprovalCall[],\n\tagentId: string,\n\ttoken: string | undefined,\n) =>\n\t| { readonly kind: 'allow' }\n\t| { readonly kind: 'approval_token_invalid'; readonly reason: string }\n\t| { readonly kind: 'approval_token_missing' } {\n\tfunction auditApproval(event: Omit<PortalApprovalAuditEvent, 'kind' | 'timeMs'>): void {\n\t\tconst auditEvent = { ...event, kind: 'mcp_portal_approval', timeMs: Date.now() } as const;\n\t\ttry {\n\t\t\tprops.auditSink?.(auditEvent);\n\t\t} catch (error) {\n\t\t\tprops.auditErrorSink?.(error instanceof Error ? error : new Error(String(error)), auditEvent);\n\t\t}\n\t}\n\n\tconst consumedApprovalTokenIds = new Map<string, number>();\n\tconst replayCacheLimit = props.approvalTokenReplayCacheLimit ?? approvalTokenReplayCacheLimit;\n\tconst consumeTokenId = (\n\t\tagentId: string,\n\t\tjti: string,\n\t\texpiresAtMs: number,\n\t):\n\t\t| { readonly ok: true }\n\t\t| { readonly ok: false; readonly reason: 'replay-cache-full' | 'replayed' } => {\n\t\tconst nowMs = Date.now();\n\t\tfor (const [tokenKey, tokenExpiresAtMs] of consumedApprovalTokenIds) {\n\t\t\tif (tokenExpiresAtMs <= nowMs) {\n\t\t\t\tconsumedApprovalTokenIds.delete(tokenKey);\n\t\t\t}\n\t\t}\n\t\tconst tokenKey = `${agentId}\\n${jti}`;\n\t\tif (consumedApprovalTokenIds.has(tokenKey)) {\n\t\t\treturn { ok: false, reason: 'replayed' };\n\t\t}\n\t\tif (consumedApprovalTokenIds.size >= replayCacheLimit) {\n\t\t\treturn { ok: false, reason: 'replay-cache-full' };\n\t\t}\n\t\tconsumedApprovalTokenIds.set(tokenKey, expiresAtMs);\n\t\treturn { ok: true };\n\t};\n\treturn (calls, agentId, token) => {\n\t\tconst record = props.records.get(agentId);\n\t\tif (record === undefined) {\n\t\t\tauditApproval({\n\t\t\t\tagentId,\n\t\t\t\tdecision: 'deny',\n\t\t\t\treason: 'approval_token_invalid',\n\t\t\t\tverifierReason: 'unknown-agent',\n\t\t\t});\n\t\t\treturn { kind: 'approval_token_invalid', reason: 'unknown-agent' };\n\t\t}\n\t\tconst callsRequiringApproval = calls.filter((call) =>\n\t\t\tapprovalRequiredByProfile(record.profile, call),\n\t\t);\n\t\tif (callsRequiringApproval.length === 0) {\n\t\t\tauditApproval({ agentId, decision: 'allow', reason: 'no_approval_required' });\n\t\t\treturn { kind: 'allow' };\n\t\t}\n\t\tif (token === undefined) {\n\t\t\tauditApproval({ agentId, decision: 'deny', reason: 'approval_token_missing' });\n\t\t\treturn { kind: 'approval_token_missing' };\n\t\t}\n\t\tconst verificationProps = {\n\t\t\tagentId,\n\t\t\tcalls: approvalTokenCallDigests(callsRequiringApproval),\n\t\t\tconsumeTokenId: (jti: string, expiresAtMs: number) =>\n\t\t\t\tconsumeTokenId(agentId, jti, expiresAtMs),\n\t\t\tkey: record.hmacKey,\n\t\t\tmaxLifetimeMs: approvalTokenMaxLifetimeMs,\n\t\t\tnowMs: Date.now(),\n\t\t\ttoken,\n\t\t};\n\t\tconst verification = verifyApprovalToken(verificationProps);\n\t\tif (verification.ok) {\n\t\t\tauditApproval({ agentId, decision: 'allow' });\n\t\t\treturn { kind: 'allow' };\n\t\t}\n\t\tauditApproval({\n\t\t\tagentId,\n\t\t\tdecision: 'deny',\n\t\t\treason: 'approval_token_invalid',\n\t\t\tverifierReason: verification.reason,\n\t\t});\n\t\treturn { kind: 'approval_token_invalid', reason: verification.reason };\n\t};\n}\n"],"mappings":"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"}
|