@agent-vm/openclaw-mcp-portal-plugin 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/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
- import { join } from "node:path";
2
1
  import { loadMcpConfig, loadMcpPortalConfig, mcpPortalCallRequiresApproval, resolveMcpPortalProfile } from "@agent-vm/config-contracts";
3
- import { hashCallArguments, portalHmacKeyEnvName, redactCredentialText, signApprovalToken } from "@agent-vm/mcp-portal";
4
- import { randomBytes } from "node:crypto";
2
+ import { createPortalCore, createUpstreamMcpClientRuntime, listPortalCoreToolDescriptors, redactCredentialText, resolveUpstreamServers } from "@agent-vm/mcp-portal/core";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
5
  import { z } from "zod";
6
- import { spawn } from "node:child_process";
7
6
  //#region src/before-prompt-build-handler.ts
8
7
  function createBeforePromptBuildHandler(props) {
9
8
  return async (_event, context) => {
@@ -24,26 +23,6 @@ function createBeforePromptBuildHandler(props) {
24
23
  }
25
24
  //#endregion
26
25
  //#region src/portal-tool-policy.ts
27
- function encodePortalServerNameSegment(value) {
28
- const encodedCharacters = [];
29
- for (let index = 0; index < value.length; index += 1) {
30
- const character = value.charAt(index);
31
- if (/^[A-Za-z0-9]$/u.test(character)) encodedCharacters.push(character);
32
- else encodedCharacters.push(`_${character.charCodeAt(0).toString(16).padStart(2, "0")}_`);
33
- }
34
- return encodedCharacters.join("");
35
- }
36
- function portalServerNameForAgent(agentId) {
37
- return `mcp_portal_${encodePortalServerNameSegment(agentId)}`;
38
- }
39
- function materializedPortalToolNames(serverName) {
40
- return [
41
- `${serverName}__mcp_portal_list`,
42
- `${serverName}__mcp_portal_search`,
43
- `${serverName}__mcp_portal_describe`,
44
- `${serverName}__mcp_portal_call`
45
- ];
46
- }
47
26
  function profileAllowsPortalCall(profile, call) {
48
27
  if (!profile.enabledNamespaces.includes(call.namespace)) return false;
49
28
  const enabledTools = profile.enabledToolsByNamespace[call.namespace] ?? [];
@@ -55,17 +34,16 @@ function profileRequiresPortalApproval(profile, call) {
55
34
  }
56
35
  //#endregion
57
36
  //#region src/before-tool-call-handler.ts
58
- const approvalTokenTtlMs = 6e4;
59
- function isObjectRecord$1(value) {
37
+ function isObjectRecord$2(value) {
60
38
  return typeof value === "object" && value !== null && !Array.isArray(value);
61
39
  }
62
40
  function parseCallRequest(value) {
63
- if (!isObjectRecord$1(value)) return null;
41
+ if (!isObjectRecord$2(value)) return null;
64
42
  const id = value.id;
65
43
  const namespace = value.namespace;
66
44
  const toolName = value.toolName;
67
45
  const argumentsValue = value.arguments;
68
- if (typeof id !== "string" || typeof namespace !== "string" || typeof toolName !== "string" || !isObjectRecord$1(argumentsValue)) return null;
46
+ if (typeof id !== "string" || typeof namespace !== "string" || typeof toolName !== "string" || !isObjectRecord$2(argumentsValue)) return null;
69
47
  return {
70
48
  arguments: argumentsValue,
71
49
  id,
@@ -73,9 +51,6 @@ function parseCallRequest(value) {
73
51
  toolName
74
52
  };
75
53
  }
76
- function portalAgentIdFromToolName(toolName, agentIds) {
77
- return agentIds.find((agentId) => toolName.startsWith(`${portalServerNameForAgent(agentId)}__mcp_portal_`)) ?? null;
78
- }
79
54
  function parseCallRequests(params) {
80
55
  const calls = params.calls;
81
56
  if (!Array.isArray(calls)) return null;
@@ -87,28 +62,15 @@ function parseCallRequests(params) {
87
62
  }
88
63
  return parsedCalls;
89
64
  }
90
- function errorMessage$1(error) {
91
- return error instanceof Error ? error.message : String(error);
92
- }
93
65
  function createBeforeToolCallHandler(props) {
94
66
  return async (event, context) => {
95
- const portalConfig = await props.runtimeState.loadPortalConfig();
96
- const agentId = portalAgentIdFromToolName(event.toolName, Object.keys(portalConfig.agents));
97
- if (agentId === null) return;
98
- const portalUnavailableReason = props.runtimeState.getPortalUnavailableReason();
99
- if (portalUnavailableReason !== null) return {
100
- block: true,
101
- blockReason: `mcp-portal: portal subprocess unavailable (${portalUnavailableReason}).`
102
- };
67
+ if (event.toolName !== "mcp_portal_call") return;
103
68
  if (context.agentId === void 0) return {
104
69
  block: true,
105
70
  blockReason: `mcp-portal: missing OpenClaw agent context for ${event.toolName}.`
106
71
  };
107
- if (context.agentId !== agentId) return {
108
- block: true,
109
- blockReason: `mcp-portal: tool ${event.toolName} is not assigned to agent ${context.agentId}.`
110
- };
111
- if (!event.toolName.endsWith("__mcp_portal_call")) return;
72
+ const portalConfig = await props.runtimeState.loadPortalConfig();
73
+ const agentId = context.agentId;
112
74
  const agent = portalConfig.agents[agentId];
113
75
  if (agent === void 0) return {
114
76
  block: true,
@@ -126,29 +88,6 @@ function createBeforeToolCallHandler(props) {
126
88
  };
127
89
  const approvalCalls = calls.filter((call) => profileRequiresPortalApproval(profile, call));
128
90
  if (approvalCalls.length === 0) return;
129
- const token = signApprovalToken({
130
- agentId,
131
- calls: approvalCalls.map((call) => ({
132
- argumentsHash: hashCallArguments(call.arguments),
133
- namespace: call.namespace,
134
- toolName: call.toolName
135
- })),
136
- expiresAtMs: Date.now() + approvalTokenTtlMs,
137
- key: props.runtimeState.getKeyRegistry().getKey(agentId)
138
- });
139
- try {
140
- event.params.portalApprovalToken = token;
141
- } catch (error) {
142
- props.logger?.warn?.(`[mcp-portal] could not attach server-side approval token: ${errorMessage$1(error)}`);
143
- return {
144
- block: true,
145
- blockReason: "mcp-portal: could not attach server-side approval token."
146
- };
147
- }
148
- if (event.params.portalApprovalToken !== token) return {
149
- block: true,
150
- blockReason: "mcp-portal: could not attach server-side approval token."
151
- };
152
91
  const toolNames = approvalCalls.map((call) => `${call.namespace}.${call.toolName}`).toSorted().join(", ");
153
92
  return { requireApproval: {
154
93
  description: `Allow MCP Portal batch for agent ${agentId}: ${toolNames}.`,
@@ -161,300 +100,86 @@ function createBeforeToolCallHandler(props) {
161
100
  };
162
101
  }
163
102
  //#endregion
164
- //#region src/hmac-key-registry.ts
165
- const hmacKeyBytes = 32;
166
- function createHmacKeyRegistry(props) {
167
- const keysByAgent = /* @__PURE__ */ new Map();
168
- for (const agentId of props.agentIds) keysByAgent.set(agentId, randomBytes(hmacKeyBytes));
103
+ //#region src/effective-config-manifest.ts
104
+ const effectiveConfigManifestFileName = "mcp-portal-effective-manifest.json";
105
+ function isObjectRecord$1(value) {
106
+ return typeof value === "object" && value !== null && !Array.isArray(value);
107
+ }
108
+ function isSafeManifestFileName(value) {
109
+ return value.length > 0 && !value.includes("/") && !value.includes("\\");
110
+ }
111
+ function parseEffectiveConfigManifest(value) {
112
+ if (!isObjectRecord$1(value)) throw new Error("MCP Portal effective config manifest must be an object.");
113
+ if (value.schemaVersion !== 1) throw new Error("MCP Portal effective config manifest must use schemaVersion 1.");
114
+ if (typeof value.mcpConfigFile !== "string" || !isSafeManifestFileName(value.mcpConfigFile)) throw new Error("MCP Portal effective config manifest must contain a safe mcpConfigFile.");
115
+ if (typeof value.portalConfigFile !== "string" || !isSafeManifestFileName(value.portalConfigFile)) throw new Error("MCP Portal effective config manifest must contain a safe portalConfigFile.");
169
116
  return {
170
- agentIds: [...props.agentIds],
171
- getKey: (agentId) => {
172
- const key = keysByAgent.get(agentId);
173
- if (key === void 0) throw new Error(`HMAC key registry: unknown agent "${agentId}".`);
174
- return key;
175
- },
176
- serializeForEnv: () => Object.fromEntries([...keysByAgent.entries()].map(([agentId, key]) => [portalHmacKeyEnvName(agentId), key.toString("hex")]))
117
+ mcpConfigFile: value.mcpConfigFile,
118
+ portalConfigFile: value.portalConfigFile
119
+ };
120
+ }
121
+ function isMissingFileError(error) {
122
+ return isObjectRecord$1(error) && typeof error.code === "string" && (error.code === "ENOENT" || error.code === "ENOTDIR");
123
+ }
124
+ async function resolveEffectiveConfigPaths(configDir) {
125
+ const manifestPath = join(configDir, effectiveConfigManifestFileName);
126
+ let manifestText;
127
+ try {
128
+ manifestText = await readFile(manifestPath, "utf8");
129
+ } catch (error) {
130
+ if (isMissingFileError(error)) return {
131
+ mcpConfigPath: join(configDir, "mcp.config.jsonc"),
132
+ portalConfigPath: join(configDir, "mcp-portal.config.jsonc")
133
+ };
134
+ throw error;
135
+ }
136
+ const manifest = parseEffectiveConfigManifest(JSON.parse(manifestText));
137
+ return {
138
+ mcpConfigPath: join(configDir, manifest.mcpConfigFile),
139
+ portalConfigPath: join(configDir, manifest.portalConfigFile)
177
140
  };
178
141
  }
179
142
  //#endregion
180
143
  //#region src/portal-config.ts
181
- const defaultPortalBinPath = "/opt/agent-vm/portal/bin/agent-vm-mcp-portal-server";
182
- const portalPluginConfigSchema = z.object({
183
- binPath: z.string().min(1).default(defaultPortalBinPath),
184
- configDir: z.string().min(1).optional()
185
- }).strict();
144
+ const portalPluginConfigSchema = z.object({ configDir: z.string().min(1) }).strict();
186
145
  function parsePortalConfig(value) {
187
146
  return portalPluginConfigSchema.parse(value ?? {});
188
147
  }
189
148
  //#endregion
190
149
  //#region src/portal-plugin-runtime-state.ts
191
150
  function createPortalPluginRuntimeState(props) {
192
- let keyRegistry = null;
151
+ let loadedPortalConfig = null;
193
152
  let portalConfigPromise = null;
194
153
  let portalUnavailableReason = null;
195
154
  const loadPortalConfigFile = props.loadPortalConfig ?? loadMcpPortalConfig;
196
- const portalConfigPath = join(props.configDir, "mcp-portal.config.jsonc");
197
155
  function loadPortalConfig() {
198
156
  if (portalConfigPromise !== null) return portalConfigPromise;
199
- const nextPromise = loadPortalConfigFile(portalConfigPath).catch((error) => {
157
+ const nextPromise = resolveEffectiveConfigPaths(props.configDir).then((effectiveConfigPaths) => loadPortalConfigFile(effectiveConfigPaths.portalConfigPath)).then((portalConfig) => {
158
+ loadedPortalConfig = portalConfig;
159
+ return portalConfig;
160
+ }).catch((error) => {
200
161
  if (portalConfigPromise === nextPromise) portalConfigPromise = null;
201
162
  throw error;
202
163
  });
203
164
  portalConfigPromise = nextPromise;
204
- return nextPromise;
165
+ return portalConfigPromise;
205
166
  }
206
167
  return {
207
168
  configDir: props.configDir,
169
+ getLoadedPortalConfig: () => loadedPortalConfig,
208
170
  getPortalUnavailableReason: () => portalUnavailableReason,
209
- getKeyRegistry: () => {
210
- if (keyRegistry === null) throw new Error("MCP Portal HMAC key registry is not initialized.");
211
- return keyRegistry;
212
- },
213
171
  loadPortalConfig,
214
172
  markPortalAvailable: () => {
215
173
  portalUnavailableReason = null;
216
174
  },
217
175
  markPortalUnavailable: (reason) => {
218
176
  portalUnavailableReason = reason;
219
- },
220
- setKeyRegistry: (registry) => {
221
- keyRegistry = registry;
222
- }
223
- };
224
- }
225
- //#endregion
226
- //#region src/portal-subprocess-supervisor.ts
227
- const defaultBackoffSteps = [
228
- 200,
229
- 400,
230
- 800,
231
- 1600,
232
- 3200,
233
- 5e3
234
- ];
235
- const inheritedPortalEnvNames = [
236
- "HOME",
237
- "PATH",
238
- "TEMP",
239
- "TMP",
240
- "TMPDIR"
241
- ];
242
- function createPortalSubprocessEnv(hmacEnv, portalEnv = {}) {
243
- const env = {};
244
- for (const name of inheritedPortalEnvNames) {
245
- const value = process.env[name];
246
- if (value !== void 0) env[name] = value;
247
- }
248
- return {
249
- ...env,
250
- ...portalEnv,
251
- ...hmacEnv
252
- };
253
- }
254
- function logSubprocessOutput(props) {
255
- const text = String(props.chunk);
256
- for (const line of text.split(/\r?\n/u)) {
257
- if (line.length === 0) continue;
258
- const message = `[mcp-portal ${props.streamName}] ${line}`;
259
- if (props.streamName === "stderr") props.logger.warn(message);
260
- else props.logger.info(message);
261
- }
262
- }
263
- function delay(ms) {
264
- return new Promise((resolve) => {
265
- setTimeout(resolve, ms);
266
- });
267
- }
268
- async function waitForExit(child, timeoutMs) {
269
- return new Promise((resolve) => {
270
- let settled = false;
271
- const timer = setTimeout(() => {
272
- if (settled) return;
273
- settled = true;
274
- child.off("exit", handleExit);
275
- resolve(false);
276
- }, timeoutMs);
277
- const handleExit = () => {
278
- if (settled) return;
279
- settled = true;
280
- clearTimeout(timer);
281
- resolve(true);
282
- };
283
- child.once("exit", handleExit);
284
- });
285
- }
286
- async function waitForHealthAttempt(props) {
287
- if (Date.now() - props.startedAt > props.timeoutMs) {
288
- const message = props.lastError instanceof Error ? props.lastError.message : String(props.lastError);
289
- throw new Error(`Timed out waiting for MCP Portal health: ${message}`);
290
- }
291
- try {
292
- const response = await props.fetchFn(`http://${props.host}:${String(props.port)}/health`);
293
- if (response.ok) return;
294
- await delay(props.intervalMs);
295
- return waitForHealthAttempt({
296
- ...props,
297
- lastError: /* @__PURE__ */ new Error(`health returned ${String(response.status)}`)
298
- });
299
- } catch (error) {
300
- await delay(props.intervalMs);
301
- return waitForHealthAttempt({
302
- ...props,
303
- lastError: error
304
- });
305
- }
306
- }
307
- async function waitForHealth(props) {
308
- const startedAt = Date.now();
309
- return waitForHealthAttempt({
310
- ...props,
311
- lastError: void 0,
312
- startedAt
313
- });
314
- }
315
- function errorMessage(error) {
316
- return error instanceof Error ? error.message : String(error);
317
- }
318
- function createPortalSubprocessSupervisor(props) {
319
- const spawnFn = props.spawnFn ?? ((command, args, options) => spawn(command, [...args], options));
320
- const fetchFn = props.fetchFn ?? fetch;
321
- const healthPollIntervalMs = props.healthPollIntervalMs ?? 200;
322
- const healthTimeoutMs = props.healthTimeoutMs ?? 1e4;
323
- const stopGraceMs = props.stopGraceMs ?? 5e3;
324
- const maxRestarts = props.maxRestarts ?? 5;
325
- const backoffSteps = props.backoffSteps ?? defaultBackoffSteps;
326
- let child = null;
327
- let stopping = false;
328
- let restartCount = 0;
329
- const spawnChild = () => {
330
- const nextChild = spawnFn(props.binPath, ["--config-dir", props.configDir], {
331
- env: createPortalSubprocessEnv(props.hmacEnv, props.portalEnv),
332
- stdio: [
333
- "ignore",
334
- "pipe",
335
- "pipe"
336
- ]
337
- });
338
- let autoRestartEnabled = false;
339
- let failureHandled = false;
340
- let rejectEarlyFailure;
341
- const earlyFailure = new Promise((_resolve, reject) => {
342
- rejectEarlyFailure = reject;
343
- });
344
- const rejectBeforeHealth = (error) => {
345
- if (rejectEarlyFailure === void 0) throw new Error("MCP Portal early-failure rejector was not initialized.");
346
- rejectEarlyFailure(error);
347
- };
348
- child = nextChild;
349
- nextChild.stdout?.on("data", (chunk) => {
350
- logSubprocessOutput({
351
- chunk,
352
- logger: props.logger,
353
- streamName: "stdout"
354
- });
355
- });
356
- nextChild.stdout?.on("error", (error) => {
357
- props.logger.warn(`[mcp-portal stdout] stream error: ${error.message}`);
358
- });
359
- nextChild.stderr?.on("data", (chunk) => {
360
- logSubprocessOutput({
361
- chunk,
362
- logger: props.logger,
363
- streamName: "stderr"
364
- });
365
- });
366
- nextChild.stderr?.on("error", (error) => {
367
- props.logger.warn(`[mcp-portal stderr] stream error: ${error.message}`);
368
- });
369
- nextChild.on("error", (error) => {
370
- props.logger.error(`[mcp-portal] subprocess spawn failed: ${error.message}`);
371
- if (failureHandled) return;
372
- failureHandled = true;
373
- if (child === nextChild) child = null;
374
- if (stopping) return;
375
- if (autoRestartEnabled) scheduleRestart();
376
- else rejectBeforeHealth(error);
377
- });
378
- nextChild.on("exit", (code, signal) => {
379
- if (failureHandled) return;
380
- failureHandled = true;
381
- if (child === nextChild) child = null;
382
- if (stopping) return;
383
- if (autoRestartEnabled) scheduleRestart();
384
- else rejectBeforeHealth(/* @__PURE__ */ new Error(`MCP Portal subprocess exited before health check completed (code=${String(code)} signal=${String(signal)}).`));
385
- });
386
- return {
387
- child: nextChild,
388
- earlyFailure,
389
- enableAutoRestart: () => {
390
- autoRestartEnabled = true;
391
- }
392
- };
393
- };
394
- const spawnChildAndWaitForHealth = async () => {
395
- const spawnedChild = spawnChild();
396
- try {
397
- await Promise.race([waitForHealth({
398
- fetchFn,
399
- host: props.host,
400
- intervalMs: healthPollIntervalMs,
401
- port: props.port,
402
- timeoutMs: healthTimeoutMs
403
- }), spawnedChild.earlyFailure]);
404
- } catch (error) {
405
- if (child === spawnedChild.child) {
406
- child = null;
407
- if (!spawnedChild.child.killed) spawnedChild.child.kill("SIGTERM");
408
- }
409
- throw error;
410
- }
411
- spawnedChild.enableAutoRestart();
412
- restartCount = 0;
413
- props.logger.info("[mcp-portal] subprocess is healthy.");
414
- };
415
- const scheduleRestart = async () => {
416
- restartCount += 1;
417
- if (restartCount > maxRestarts) {
418
- props.logger.error("[mcp-portal] subprocess restart limit exhausted.");
419
- props.onFatal?.("backoff-exhausted");
420
- return;
421
- }
422
- const backoffMs = backoffSteps[Math.min(restartCount - 1, backoffSteps.length - 1)] ?? backoffSteps[backoffSteps.length - 1] ?? 5e3;
423
- props.logger.warn(`[mcp-portal] subprocess exited; restarting in ${String(backoffMs)}ms.`);
424
- await delay(backoffMs);
425
- if (stopping) return;
426
- try {
427
- await spawnChildAndWaitForHealth();
428
- } catch (error) {
429
- props.logger.error(`[mcp-portal] subprocess restart failed: ${errorMessage(error)}`);
430
- if (!stopping) await scheduleRestart();
431
- }
432
- };
433
- return {
434
- isAlive: () => child !== null && !child.killed,
435
- start: async () => {
436
- stopping = false;
437
- await spawnChildAndWaitForHealth();
438
- },
439
- stop: async () => {
440
- stopping = true;
441
- const activeChild = child;
442
- child = null;
443
- if (activeChild === null || activeChild.killed) return;
444
- activeChild.kill("SIGTERM");
445
- if (!await waitForExit(activeChild, stopGraceMs) && !activeChild.killed) activeChild.kill("SIGKILL");
446
177
  }
447
178
  };
448
179
  }
449
180
  //#endregion
450
181
  //#region src/plugin-registration.ts
451
182
  const pluginId = "mcp-portal";
452
- const onePasswordCliEnvNames = [
453
- "OP_SERVICE_ACCOUNT_TOKEN",
454
- "OP_ACCOUNT",
455
- "OP_CONNECT_HOST",
456
- "OP_CONNECT_TOKEN"
457
- ];
458
183
  function hasFunction(value) {
459
184
  return typeof value === "function";
460
185
  }
@@ -467,80 +192,16 @@ function isUnknownArray(value) {
467
192
  function getObjectProperty(value, property) {
468
193
  return isObjectRecord(value) ? value[property] : void 0;
469
194
  }
470
- function messageFromUnknown(error) {
471
- return error instanceof Error ? error.message : String(error);
472
- }
473
- function addEnvironmentSecretName(names, secret) {
474
- if (secret.source === "environment") names.add(secret.name);
475
- }
476
- function secretUsesOnePassword(secret) {
477
- return secret.source === "1password";
478
- }
479
- function collectMcpConfigEnvironmentSecretNames(config) {
480
- const names = /* @__PURE__ */ new Set();
481
- for (const provider of Object.values(config.providers)) {
482
- const transport = provider.transport;
483
- const secrets = transport.kind === "stdio" ? Object.values(transport.env) : Object.values(transport.headers);
484
- for (const secret of secrets) addEnvironmentSecretName(names, secret);
485
- }
486
- return names;
487
- }
488
- function collectMcpPortalConfigEnvironmentSecretNames(config) {
489
- const names = /* @__PURE__ */ new Set();
490
- addEnvironmentSecretName(names, config.server.accessHeader.secret);
491
- for (const agent of Object.values(config.agents)) if (agent.hmacKey !== void 0) addEnvironmentSecretName(names, agent.hmacKey);
492
- return names;
493
- }
494
- function mcpConfigUsesOnePassword(config) {
495
- return Object.values(config.providers).some((provider) => {
496
- const transport = provider.transport;
497
- return (transport.kind === "stdio" ? Object.values(transport.env) : Object.values(transport.headers)).some(secretUsesOnePassword);
498
- });
499
- }
500
- function mcpPortalConfigUsesOnePassword(config) {
501
- return secretUsesOnePassword(config.server.accessHeader.secret) || Object.values(config.agents).some((agent) => agent.hmacKey !== void 0 && secretUsesOnePassword(agent.hmacKey));
502
- }
503
- function resolveRequiredPortalEnv(props) {
504
- const resolvedEnv = {};
505
- for (const name of [...props.names].toSorted()) {
506
- const value = props.env[name];
507
- if (value === void 0 || value.length === 0) throw new Error(`Missing environment secret ${name} for MCP Portal subprocess.`);
508
- resolvedEnv[name] = value;
509
- }
510
- return resolvedEnv;
511
- }
512
- function createPortalSubprocessConfigEnv(props) {
513
- const env = props.env ?? process.env;
514
- const portalEnv = { ...resolveRequiredPortalEnv({
515
- env,
516
- names: new Set([...collectMcpConfigEnvironmentSecretNames(props.mcpConfig), ...collectMcpPortalConfigEnvironmentSecretNames(props.mcpPortalConfig)])
517
- }) };
518
- if (mcpConfigUsesOnePassword(props.mcpConfig) || mcpPortalConfigUsesOnePassword(props.mcpPortalConfig)) for (const name of onePasswordCliEnvNames) {
519
- const value = env[name];
520
- if (value !== void 0 && value.length > 0) portalEnv[name] = value;
521
- }
522
- return portalEnv;
523
- }
524
195
  function resolveConfigDir(api) {
525
- const pluginConfig = parsePortalConfig(api.pluginConfig ?? {});
526
- if (pluginConfig.configDir !== void 0) return pluginConfig.configDir;
527
- const topLevelMcpConfigDir = getObjectProperty(getObjectProperty(api.config, "mcp"), "configDir");
196
+ if (api.pluginConfig !== void 0) return parsePortalConfig(api.pluginConfig).configDir;
197
+ const topLevelMcpConfigDir = getObjectProperty(getObjectProperty(api.config, "mcpPortal"), "configDir");
528
198
  if (typeof topLevelMcpConfigDir === "string" && topLevelMcpConfigDir.length > 0) return topLevelMcpConfigDir;
529
199
  const zones = getObjectProperty(api.config, "zones");
530
200
  if (isUnknownArray(zones)) {
531
- const zoneMcpConfigDir = getObjectProperty(getObjectProperty(zones.at(0), "mcp"), "configDir");
201
+ const zoneMcpConfigDir = getObjectProperty(getObjectProperty(zones.at(0), "mcpPortal"), "configDir");
532
202
  if (typeof zoneMcpConfigDir === "string" && zoneMcpConfigDir.length > 0) return zoneMcpConfigDir;
533
203
  }
534
- throw new Error("MCP Portal plugin requires configDir in plugin config or zone mcp config.");
535
- }
536
- function tcpPoolConfigFromApi(api) {
537
- const tcpPool = getObjectProperty(api.config, "tcpPool");
538
- const basePort = getObjectProperty(tcpPool, "basePort");
539
- const size = getObjectProperty(tcpPool, "size");
540
- return typeof basePort === "number" && typeof size === "number" ? {
541
- basePort,
542
- size
543
- } : null;
204
+ throw new Error("MCP Portal plugin requires configDir in plugin config or zone mcpPortal config.");
544
205
  }
545
206
  function validatePortalPortAgainstTcpPool(props) {
546
207
  if (props.tcpPool === null) return;
@@ -556,8 +217,8 @@ function createLoggerAdapter(api) {
556
217
  };
557
218
  }
558
219
  function validatePortalPluginApi(api) {
559
- if (!hasFunction(api.registerService)) throw new Error("MCP Portal plugin requires OpenClaw registerService API.");
560
- if (!hasFunction(api.on) && !hasFunction(api.registerPromptHook)) throw new Error("MCP Portal plugin requires OpenClaw prompt hook registration API.");
220
+ if (!hasFunction(api.registerTool)) throw new Error("MCP Portal plugin requires OpenClaw registerTool API.");
221
+ if (!hasFunction(api.on)) throw new Error("MCP Portal plugin requires OpenClaw before_tool_call hook API.");
561
222
  if (hasFunction(api.lifecycle?.registerRuntimeLifecycle) || hasFunction(api.registerRuntimeLifecycle)) return;
562
223
  throw new Error("MCP Portal plugin requires an OpenClaw lifecycle cleanup API.");
563
224
  }
@@ -566,8 +227,8 @@ function registerPortalRuntimeCleanup(api, cleanup) {
566
227
  cleanup: async () => {
567
228
  await cleanup();
568
229
  },
569
- description: "Stops the MCP Portal subprocess supervised by the agent-vm plugin.",
570
- id: "mcp-portal-subprocess"
230
+ description: "Closes MCP Portal upstream clients owned by the agent-vm plugin.",
231
+ id: "mcp-portal-core"
571
232
  };
572
233
  if (hasFunction(api.lifecycle?.registerRuntimeLifecycle)) {
573
234
  api.lifecycle.registerRuntimeLifecycle(runtimeLifecycle);
@@ -579,53 +240,163 @@ function registerPortalRuntimeCleanup(api, cleanup) {
579
240
  }
580
241
  throw new Error("MCP Portal plugin requires an OpenClaw lifecycle cleanup API.");
581
242
  }
582
- function registerPortalService(props) {
583
- const portalConfig = parsePortalConfig(props.api.pluginConfig ?? {});
584
- let supervisor = null;
585
- props.api.registerService?.({
586
- id: "mcp-portal-subprocess",
587
- start: async () => {
588
- const mcpPortalConfig = await props.runtimeState.loadPortalConfig();
589
- const mcpConfig = await loadMcpConfig(join(props.configDir, "mcp.config.jsonc"));
590
- validatePortalPortAgainstTcpPool({
591
- port: mcpPortalConfig.server.port,
592
- tcpPool: tcpPoolConfigFromApi(props.api)
593
- });
594
- const keyRegistry = createHmacKeyRegistry({ agentIds: Object.keys(mcpPortalConfig.agents).toSorted() });
595
- props.runtimeState.setKeyRegistry(keyRegistry);
596
- supervisor = createPortalSubprocessSupervisor({
597
- binPath: portalConfig.binPath,
598
- configDir: props.configDir,
599
- host: mcpPortalConfig.server.host,
600
- hmacEnv: keyRegistry.serializeForEnv(),
601
- logger: createLoggerAdapter(props.api),
602
- onFatal: (reason) => {
603
- props.runtimeState.markPortalUnavailable(reason);
604
- props.api.logger?.error?.(`[mcp-portal] subprocess supervisor fatal: ${reason}`);
605
- },
606
- port: mcpPortalConfig.server.port,
607
- portalEnv: createPortalSubprocessConfigEnv({
608
- mcpConfig,
609
- mcpPortalConfig
610
- })
243
+ function selectorsFromNamespaceTools(namespaceTools) {
244
+ return Object.entries(namespaceTools).flatMap(([namespace, toolNames]) => toolNames.map((toolName) => ({
245
+ namespace,
246
+ toolName
247
+ })));
248
+ }
249
+ function buildProfilePolicyMaps(portalConfig) {
250
+ const enabledNamespacesByAgent = {};
251
+ const enabledToolsByAgent = {};
252
+ const hiddenToolsByAgent = {};
253
+ const profileTtls = [];
254
+ for (const [agentId, agent] of Object.entries(portalConfig.agents)) {
255
+ const profile = resolveMcpPortalProfile(portalConfig, agent.profile);
256
+ enabledNamespacesByAgent[agentId] = profile.enabledNamespaces;
257
+ enabledToolsByAgent[agentId] = selectorsFromNamespaceTools(profile.enabledToolsByNamespace);
258
+ hiddenToolsByAgent[agentId] = selectorsFromNamespaceTools(profile.hiddenToolsByNamespace);
259
+ profileTtls.push(profile.cache.catalogTtlMs);
260
+ }
261
+ return {
262
+ cacheTtlMs: profileTtls.length === 0 ? 6e4 : Math.min(...profileTtls),
263
+ enabledNamespacesByAgent,
264
+ enabledToolsByAgent,
265
+ hiddenToolsByAgent
266
+ };
267
+ }
268
+ async function resolveManagedPortalSecret(secret) {
269
+ if (secret.source !== "environment") throw new Error("MCP Portal managed OpenClaw effective config must use environment secret refs.");
270
+ const value = process.env[secret.name];
271
+ if (value === void 0 || value.length === 0) throw new Error(`Missing environment secret ${secret.name} for MCP Portal native plugin.`);
272
+ return value;
273
+ }
274
+ async function createManagedPortalCore(configDir) {
275
+ const effectiveConfigPaths = await resolveEffectiveConfigPaths(configDir);
276
+ const [mcpConfig, portalConfig] = await Promise.all([loadMcpConfig(effectiveConfigPaths.mcpConfigPath), loadMcpPortalConfig(effectiveConfigPaths.portalConfigPath)]);
277
+ const upstreamServers = await resolveUpstreamServers({
278
+ config: mcpConfig,
279
+ resolveSecret: resolveManagedPortalSecret
280
+ });
281
+ const upstreamRuntime = createUpstreamMcpClientRuntime({ servers: upstreamServers });
282
+ const profilePolicyMaps = buildProfilePolicyMaps(portalConfig);
283
+ return createPortalCore({
284
+ accessPolicy: {
285
+ defaultPolicy: "deny-all",
286
+ enabledNamespacesByAgent: profilePolicyMaps.enabledNamespacesByAgent,
287
+ enabledToolsByAgent: profilePolicyMaps.enabledToolsByAgent,
288
+ hiddenToolsByAgent: profilePolicyMaps.hiddenToolsByAgent
289
+ },
290
+ approvalTrustBoundary: "openclaw-before-tool-call-hook",
291
+ catalogTtlMs: profilePolicyMaps.cacheTtlMs,
292
+ runtime: {
293
+ callUpstreamTool: upstreamRuntime.callTool,
294
+ closeAgentScope: upstreamRuntime.closeAgentScope,
295
+ closeSession: upstreamRuntime.closeSession,
296
+ listTools: upstreamRuntime.listTools
297
+ },
298
+ upstreamNamespaces: upstreamServers.map((server) => server.namespace)
299
+ });
300
+ }
301
+ function portalUpdateFromCoreEvent(event) {
302
+ if (event.kind === "progress") return {
303
+ message: event.message ?? "MCP Portal progress",
304
+ ...event.progress !== void 0 ? { progress: event.progress } : {},
305
+ requestId: event.requestId,
306
+ ...event.total !== void 0 ? { total: event.total } : {},
307
+ type: "mcp_portal_progress"
308
+ };
309
+ if (event.kind === "partial_content") return {
310
+ content: event.content,
311
+ requestId: event.requestId,
312
+ type: "mcp_portal_partial_content"
313
+ };
314
+ if (event.kind === "upstream_notification") return {
315
+ method: event.method,
316
+ params: event.params,
317
+ requestId: event.requestId,
318
+ type: "mcp_portal_upstream_notification"
319
+ };
320
+ return null;
321
+ }
322
+ async function forwardCoreEvent(event, logger, onUpdate) {
323
+ const update = portalUpdateFromCoreEvent(event);
324
+ if (update !== null) try {
325
+ await onUpdate?.(update);
326
+ } catch (error) {
327
+ const message = error instanceof Error ? error.message : String(error);
328
+ logger.warn(`[mcp-portal] OpenClaw onUpdate delivery failed: ${message}`);
329
+ }
330
+ }
331
+ function createNativeTool(props) {
332
+ return {
333
+ description: props.descriptor.description,
334
+ execute: async (_toolCallId, params, signal, onUpdate) => {
335
+ if (props.context.agentId === void 0 || props.context.agentId.length === 0) throw new Error("mcp-portal: OpenClaw did not provide a trusted agentId.");
336
+ const core = await props.getCore();
337
+ const scope = core.createAgentScope({
338
+ agentId: props.context.agentId,
339
+ agentScopeId: props.context.agentId,
340
+ ...props.context.sessionId ? { sessionId: props.context.sessionId } : {},
341
+ ...props.context.sessionKey ? { sessionKey: props.context.sessionKey } : {},
342
+ source: "openclaw-trusted"
611
343
  });
612
- await supervisor.start();
613
- props.runtimeState.markPortalAvailable();
344
+ const result = await core.collectPortalCoreResult(core.callStream({
345
+ input: params,
346
+ scope,
347
+ ...signal !== void 0 ? { signal } : {},
348
+ toolName: props.descriptor.name
349
+ }), { onEvent: (event) => forwardCoreEvent(event, props.logger, onUpdate) });
350
+ return {
351
+ content: JSON.stringify(result),
352
+ details: result
353
+ };
614
354
  },
615
- stop: async () => {
616
- await supervisor?.stop();
617
- }
355
+ label: props.descriptor.name,
356
+ name: props.descriptor.name,
357
+ parameters: props.descriptor.inputSchema
358
+ };
359
+ }
360
+ function descriptorsForOpenClawContext(props) {
361
+ if (props.portalConfig === null || props.context.agentId === void 0) return listPortalCoreToolDescriptors();
362
+ const agent = props.portalConfig.agents[props.context.agentId];
363
+ if (agent === void 0) return listPortalCoreToolDescriptors();
364
+ return listPortalCoreToolDescriptors(resolveMcpPortalProfile(props.portalConfig, agent.profile).enabledNamespaces);
365
+ }
366
+ function registerNativePortalTools(props) {
367
+ const descriptorNames = listPortalCoreToolDescriptors().map((descriptor) => descriptor.name);
368
+ const logger = createLoggerAdapter(props.api);
369
+ props.api.registerTool?.((context) => {
370
+ return descriptorsForOpenClawContext({
371
+ context,
372
+ portalConfig: props.runtimeState.getLoadedPortalConfig()
373
+ }).map((descriptor) => createNativeTool({
374
+ context,
375
+ descriptor,
376
+ getCore: props.getCore,
377
+ logger
378
+ }));
379
+ }, {
380
+ names: descriptorNames,
381
+ optional: true
618
382
  });
619
- return { getSupervisor: () => supervisor };
620
383
  }
621
384
  function registerMcpPortalPlugin(api) {
622
385
  if (api.registrationMode !== void 0 && api.registrationMode !== "full") return;
623
386
  validatePortalPluginApi(api);
624
387
  const configDir = resolveConfigDir(api);
625
388
  const runtimeState = createPortalPluginRuntimeState({ configDir });
626
- const registeredService = registerPortalService({
389
+ let corePromise;
390
+ const getCore = () => {
391
+ corePromise ??= createManagedPortalCore(configDir).catch((error) => {
392
+ corePromise = void 0;
393
+ throw error;
394
+ });
395
+ return corePromise;
396
+ };
397
+ registerNativePortalTools({
627
398
  api,
628
- configDir,
399
+ getCore,
629
400
  runtimeState
630
401
  });
631
402
  api.on?.("before_tool_call", createBeforeToolCallHandler({
@@ -637,13 +408,12 @@ function registerMcpPortalPlugin(api) {
637
408
  const result = await createBeforePromptBuildHandler({ runtimeState })({}, context);
638
409
  if (result?.appendSystemContext !== void 0) context.appendPrompt?.(result.appendSystemContext);
639
410
  });
640
- registerPortalRuntimeCleanup(api, () => registeredService.getSupervisor()?.stop());
641
- runtimeState.loadPortalConfig().catch((error) => {
642
- api.logger?.error?.(`[mcp-portal] failed to initialize portal config: ${messageFromUnknown(error)}`);
411
+ registerPortalRuntimeCleanup(api, async () => {
412
+ await (await corePromise?.catch(() => void 0))?.close();
643
413
  });
644
414
  }
645
415
  const pluginEntry = {
646
- description: "Supervises the MCP Portal subprocess and wires per-agent approval hooks.",
416
+ description: "Registers native OpenClaw MCP Portal tools and wires per-agent approval hooks.",
647
417
  id: pluginId,
648
418
  name: "MCP Portal",
649
419
  register: registerMcpPortalPlugin
@@ -654,7 +424,7 @@ function createPortalPromptContext(props) {
654
424
  const namespaceList = props.namespaces.length > 0 ? props.namespaces.map((entry) => `${entry.namespace}(${entry.toolCount} tools)`).join(", ") : "none configured";
655
425
  const diagnostics = props.diagnostics !== void 0 && props.diagnostics.length > 0 ? [`Discovery diagnostics: ${props.diagnostics.map((entry) => `${entry.namespace}: ${entry.message}`).join("; ")}`] : [];
656
426
  return [
657
- "MCP Portal is available as an MCP server.",
427
+ "MCP Portal is available as native OpenClaw tools.",
658
428
  "Use mcp_portal_list with requests[], mcp_portal_search with requests[],",
659
429
  "mcp_portal_describe with requests[], and mcp_portal_call with calls[].",
660
430
  "Responses are { ok, results, errors, diagnostics }; results is keyed by each request/call id and each value is discriminated by ok: true or ok: false.",
@@ -674,6 +444,6 @@ function redactPortalSecrets(text, secretValues = []) {
674
444
  //#region src/index.ts
675
445
  const OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME = "@agent-vm/openclaw-mcp-portal-plugin";
676
446
  //#endregion
677
- export { OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME, createBeforePromptBuildHandler, createBeforeToolCallHandler, createHmacKeyRegistry, createPortalPluginRuntimeState, createPortalPromptContext, createPortalSubprocessConfigEnv, createPortalSubprocessSupervisor, pluginEntry as default, defaultPortalBinPath, materializedPortalToolNames, parsePortalConfig, portalPluginConfigSchema, portalServerNameForAgent, profileAllowsPortalCall, profileRequiresPortalApproval, redactPortalSecrets, registerMcpPortalPlugin, validatePortalPluginApi, validatePortalPortAgainstTcpPool };
447
+ export { OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME, createBeforePromptBuildHandler, createBeforeToolCallHandler, createPortalPluginRuntimeState, createPortalPromptContext, pluginEntry as default, parsePortalConfig, portalPluginConfigSchema, profileAllowsPortalCall, profileRequiresPortalApproval, redactPortalSecrets, registerMcpPortalPlugin, validatePortalPluginApi, validatePortalPortAgainstTcpPool };
678
448
 
679
449
  //# sourceMappingURL=index.js.map