@alfe.ai/integrations 0.0.23 → 0.0.25

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.d.ts CHANGED
@@ -248,6 +248,20 @@ interface ConfigSchemaField {
248
248
  /** Only show this field if a specific integration is installed */
249
249
  depends_on_integration?: string;
250
250
  }
251
+ interface McpServerDeclaration {
252
+ /** Unique identifier within this integration */
253
+ id: string;
254
+ /** Command to spawn (e.g., 'npx', 'node', 'xero-mcp-proxy') */
255
+ command: string;
256
+ /** Arguments for the command */
257
+ args?: string[];
258
+ /** Environment variables — supports {{config.KEY}} interpolation from config + secrets */
259
+ env?: Record<string, string>;
260
+ /** Working directory (optional) */
261
+ cwd?: string;
262
+ /** If true, applier skips auto-application — a lifecycle hook manages this MCP server instead */
263
+ hook_managed?: boolean;
264
+ }
251
265
  interface CommandDeclaration {
252
266
  /** Dot-namespaced command name (e.g. "support.diagnostic") */
253
267
  name: string;
@@ -329,6 +343,8 @@ interface IntegrationManifest {
329
343
  capabilities: string[];
330
344
  hooks: IntegrationHooks;
331
345
  commands: CommandDeclaration[];
346
+ /** MCP servers to configure in the agent runtime */
347
+ mcp_servers: McpServerDeclaration[];
332
348
  /** Git repository URL (HTTPS) — not present in YAML, injected by the publish API */
333
349
  repository?: string;
334
350
  /**
@@ -388,6 +404,10 @@ interface RuntimeApplier {
388
404
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
389
405
  /** Remove config previously applied by an integration */
390
406
  removeConfig(integrationId: string): Promise<void>;
407
+ /** Configure MCP servers in this runtime */
408
+ applyMcpServers(integrationId: string, servers: McpServerDeclaration[]): Promise<void>;
409
+ /** Remove MCP servers previously applied by an integration */
410
+ removeMcpServers(integrationId: string): Promise<void>;
391
411
  /** Check if this runtime is available (e.g. workspace directory exists) */
392
412
  isAvailable(): Promise<boolean>;
393
413
  }
@@ -738,6 +758,20 @@ declare class OpenClawApplier implements RuntimeApplier {
738
758
  * then removes them via `openclaw config unset`.
739
759
  */
740
760
  removeConfig(integrationId: string): Promise<void>;
761
+ /**
762
+ * Configure MCP servers in OpenClaw via `openclaw config set --batch-json`.
763
+ *
764
+ * Each server is written as `mcp.servers.{integrationId}-{serverId}.*`.
765
+ * Applied servers are tracked in the tracking file for clean removal.
766
+ */
767
+ applyMcpServers(integrationId: string, servers: McpServerDeclaration[]): Promise<void>;
768
+ /**
769
+ * Remove MCP servers previously applied by an integration.
770
+ *
771
+ * Reads the tracking file to find which `mcp.servers.*` keys this integration set,
772
+ * then removes them via `openclaw config unset`.
773
+ */
774
+ removeMcpServers(integrationId: string): Promise<void>;
741
775
  isAvailable(): Promise<boolean>;
742
776
  private readTracking;
743
777
  private writeTracking;
package/dist/index.js CHANGED
@@ -808,6 +808,25 @@ function interpolateSelfConfig(obj, config) {
808
808
  return result;
809
809
  }
810
810
  /**
811
+ * Interpolate `{{config.KEY}}` patterns in MCP server env vars using
812
+ * the merged config + secrets dict. Returns new declarations with
813
+ * interpolated env values; skips hook_managed servers.
814
+ */
815
+ function interpolateMcpServerEnvs(servers, mergedConfig) {
816
+ return servers.filter((s) => !s.hook_managed).map((server) => {
817
+ if (!server.env) return server;
818
+ const interpolatedEnv = {};
819
+ for (const [key, value] of Object.entries(server.env)) interpolatedEnv[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
820
+ const val = mergedConfig[configKey];
821
+ return typeof val === "string" || typeof val === "number" ? String(val) : _match;
822
+ });
823
+ return {
824
+ ...server,
825
+ env: interpolatedEnv
826
+ };
827
+ });
828
+ }
829
+ /**
811
830
  * Merge universal installs with runtime-specific installs from the manifest.
812
831
  */
813
832
  function resolveInstallsForRuntime(manifest, runtime) {
@@ -1045,6 +1064,19 @@ var IntegrationManager = class {
1045
1064
  await applier.applyConfig(integrationId, interpolatedConfig);
1046
1065
  configApplied = true;
1047
1066
  }
1067
+ const mcpServers = manifest.mcp_servers;
1068
+ if (mcpServers.length > 0) {
1069
+ const secretEntries = this.secrets.get(integrationId);
1070
+ const interpolatedServers = interpolateMcpServerEnvs(mcpServers, {
1071
+ ...entry.config,
1072
+ ...secretEntries ? Object.fromEntries(secretEntries) : {}
1073
+ });
1074
+ if (interpolatedServers.length > 0) {
1075
+ this.log.info(`Applying ${String(interpolatedServers.length)} MCP server(s) for ${integrationId} to ${runtimeName}`);
1076
+ await applier.applyMcpServers(integrationId, interpolatedServers);
1077
+ configApplied = true;
1078
+ }
1079
+ }
1048
1080
  if (plugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, plugins, skills, installPath);
1049
1081
  }
1050
1082
  this.state.setStatus(integrationId, "active");
@@ -1143,6 +1175,15 @@ var IntegrationManager = class {
1143
1175
  this.log.warn(`Failed to remove config for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1144
1176
  }
1145
1177
  }
1178
+ for (const [runtimeName, applier] of this.runtimeAppliers) {
1179
+ if (!await applier.isAvailable()) continue;
1180
+ this.log.info(`Removing MCP servers for ${integrationId} from ${runtimeName}`);
1181
+ try {
1182
+ await applier.removeMcpServers(integrationId);
1183
+ } catch (err) {
1184
+ this.log.warn(`Failed to remove MCP servers for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1185
+ }
1186
+ }
1146
1187
  this.state.setStatus(integrationId, "configured");
1147
1188
  return {
1148
1189
  ok: true,
@@ -1456,8 +1497,21 @@ var OpenClawApplier = class {
1456
1497
  "install",
1457
1498
  pkg
1458
1499
  ];
1459
- if (pkg.startsWith("@alfe.ai/")) args.push("--dangerously-force-unsafe-install");
1460
- await execFileAsync("openclaw", args, { timeout: 6e4 });
1500
+ const useUnsafeFlag = pkg.startsWith("@alfe.ai/");
1501
+ if (useUnsafeFlag) args.push("--dangerously-force-unsafe-install");
1502
+ try {
1503
+ await execFileAsync("openclaw", args, { timeout: 6e4 });
1504
+ } catch (err) {
1505
+ const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
1506
+ if (useUnsafeFlag && errText.includes("unknown option")) {
1507
+ log.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
1508
+ await execFileAsync("openclaw", [
1509
+ "plugins",
1510
+ "install",
1511
+ pkg
1512
+ ], { timeout: 6e4 });
1513
+ } else throw err;
1514
+ }
1461
1515
  } catch (err) {
1462
1516
  await new Promise((r) => {
1463
1517
  setTimeout(r, 500);
@@ -1512,8 +1566,8 @@ var OpenClawApplier = class {
1512
1566
  isPluginInstalled(pkg) {
1513
1567
  const extensionsDir = join(homedir(), ".openclaw", "extensions");
1514
1568
  if (!existsSync(extensionsDir)) return false;
1515
- const prefix = pkg.replaceAll("/", "-");
1516
- return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix));
1569
+ const prefix = pkg.replaceAll("/", "-") + "-";
1570
+ return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix) && /^[0-9a-f]+$/i.test(dir.slice(prefix.length)));
1517
1571
  }
1518
1572
  async removePlugin(pkg) {
1519
1573
  await execFileAsync("openclaw", [
@@ -1624,6 +1678,83 @@ var OpenClawApplier = class {
1624
1678
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1625
1679
  this.writeTracking(tracking);
1626
1680
  }
1681
+ /**
1682
+ * Configure MCP servers in OpenClaw via `openclaw config set --batch-json`.
1683
+ *
1684
+ * Each server is written as `mcp.servers.{integrationId}-{serverId}.*`.
1685
+ * Applied servers are tracked in the tracking file for clean removal.
1686
+ */
1687
+ async applyMcpServers(integrationId, servers) {
1688
+ if (servers.length === 0) return;
1689
+ const batch = [];
1690
+ const trackedKeys = [];
1691
+ for (const server of servers) {
1692
+ const prefix = `mcp.servers.${`${integrationId}-${server.id}`}`;
1693
+ batch.push({
1694
+ path: `${prefix}.command`,
1695
+ value: server.command
1696
+ });
1697
+ if (server.args && server.args.length > 0) batch.push({
1698
+ path: `${prefix}.args`,
1699
+ value: server.args
1700
+ });
1701
+ if (server.env) for (const [envKey, envVal] of Object.entries(server.env)) batch.push({
1702
+ path: `${prefix}.env.${envKey}`,
1703
+ value: envVal
1704
+ });
1705
+ if (server.cwd) batch.push({
1706
+ path: `${prefix}.cwd`,
1707
+ value: server.cwd
1708
+ });
1709
+ trackedKeys.push(prefix);
1710
+ }
1711
+ try {
1712
+ await execFileAsync("openclaw", [
1713
+ "config",
1714
+ "set",
1715
+ "--batch-json",
1716
+ JSON.stringify(batch),
1717
+ "--strict-json"
1718
+ ], { timeout: 1e4 });
1719
+ } catch (err) {
1720
+ log.error({
1721
+ err: err instanceof Error ? err.message : String(err),
1722
+ batch
1723
+ }, "Failed to set MCP server config via openclaw config set --batch-json");
1724
+ throw err;
1725
+ }
1726
+ const tracking = this.readTracking();
1727
+ const mcpTracking = tracking._mcpServers ?? {};
1728
+ mcpTracking[integrationId] = trackedKeys;
1729
+ tracking._mcpServers = mcpTracking;
1730
+ this.writeTracking(tracking);
1731
+ }
1732
+ /**
1733
+ * Remove MCP servers previously applied by an integration.
1734
+ *
1735
+ * Reads the tracking file to find which `mcp.servers.*` keys this integration set,
1736
+ * then removes them via `openclaw config unset`.
1737
+ */
1738
+ async removeMcpServers(integrationId) {
1739
+ const tracking = this.readTracking();
1740
+ const mcpTracking = tracking._mcpServers ?? {};
1741
+ if (!(integrationId in mcpTracking)) return;
1742
+ const prefixes = mcpTracking[integrationId];
1743
+ for (const prefix of prefixes) try {
1744
+ await execFileAsync("openclaw", [
1745
+ "config",
1746
+ "unset",
1747
+ prefix
1748
+ ], { timeout: 1e4 });
1749
+ } catch (err) {
1750
+ log.warn({
1751
+ err: err instanceof Error ? err.message : String(err),
1752
+ prefix
1753
+ }, "Failed to unset MCP server config via openclaw config unset");
1754
+ }
1755
+ tracking._mcpServers = Object.fromEntries(Object.entries(mcpTracking).filter(([key]) => key !== integrationId));
1756
+ this.writeTracking(tracking);
1757
+ }
1627
1758
  isAvailable() {
1628
1759
  return Promise.resolve(existsSync(this.workspace));
1629
1760
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",