@alfe.ai/integrations 0.0.24 → 0.0.26

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
@@ -1,7 +1,7 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { dirname, join } from "node:path";
4
- import { homedir, tmpdir } from "node:os";
4
+ import { homedir, platform, tmpdir } from "node:os";
5
5
  import { closeSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { buildConfigValidationSchema, parseManifestFile } from "@alfe.ai/integration-manifest";
7
7
  import { createLogger } from "@auriclabs/logger";
@@ -663,6 +663,7 @@ function buildHookEnv(options, additionalEnv) {
663
663
  * 3. Default → "bash"
664
664
  */
665
665
  function resolveInterpreter(scriptPath) {
666
+ const isWindows = platform() === "win32";
666
667
  try {
667
668
  const fd = openSync(scriptPath, "r");
668
669
  const buf = Buffer.alloc(256);
@@ -678,8 +679,13 @@ function resolveInterpreter(scriptPath) {
678
679
  args: [...parts.slice(1), scriptPath]
679
680
  };
680
681
  }
682
+ const cmd = shebang.split(/\s+/)[0];
683
+ if (isWindows && cmd.startsWith("/")) return {
684
+ command: cmd.split("/").pop() ?? cmd,
685
+ args: [scriptPath]
686
+ };
681
687
  return {
682
- command: shebang.split(/\s+/)[0],
688
+ command: cmd,
683
689
  args: [scriptPath]
684
690
  };
685
691
  }
@@ -689,7 +695,7 @@ function resolveInterpreter(scriptPath) {
689
695
  args: [scriptPath]
690
696
  };
691
697
  if (scriptPath.endsWith(".py")) return {
692
- command: "python3",
698
+ command: isWindows ? "python" : "python3",
693
699
  args: [scriptPath]
694
700
  };
695
701
  return {
@@ -808,6 +814,25 @@ function interpolateSelfConfig(obj, config) {
808
814
  return result;
809
815
  }
810
816
  /**
817
+ * Interpolate `{{config.KEY}}` patterns in MCP server env vars using
818
+ * the merged config + secrets dict. Returns new declarations with
819
+ * interpolated env values; skips hook_managed servers.
820
+ */
821
+ function interpolateMcpServerEnvs(servers, mergedConfig) {
822
+ return servers.filter((s) => !s.hook_managed).map((server) => {
823
+ if (!server.env) return server;
824
+ const interpolatedEnv = {};
825
+ for (const [key, value] of Object.entries(server.env)) interpolatedEnv[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
826
+ const val = mergedConfig[configKey];
827
+ return typeof val === "string" || typeof val === "number" ? String(val) : _match;
828
+ });
829
+ return {
830
+ ...server,
831
+ env: interpolatedEnv
832
+ };
833
+ });
834
+ }
835
+ /**
811
836
  * Merge universal installs with runtime-specific installs from the manifest.
812
837
  */
813
838
  function resolveInstallsForRuntime(manifest, runtime) {
@@ -1045,6 +1070,19 @@ var IntegrationManager = class {
1045
1070
  await applier.applyConfig(integrationId, interpolatedConfig);
1046
1071
  configApplied = true;
1047
1072
  }
1073
+ const mcpServers = manifest.mcp_servers ?? [];
1074
+ if (mcpServers.length > 0) {
1075
+ const secretEntries = this.secrets.get(integrationId);
1076
+ const interpolatedServers = interpolateMcpServerEnvs(mcpServers, {
1077
+ ...entry.config,
1078
+ ...secretEntries ? Object.fromEntries(secretEntries) : {}
1079
+ });
1080
+ if (interpolatedServers.length > 0) {
1081
+ this.log.info(`Applying ${String(interpolatedServers.length)} MCP server(s) for ${integrationId} to ${runtimeName}`);
1082
+ await applier.applyMcpServers(integrationId, interpolatedServers);
1083
+ configApplied = true;
1084
+ }
1085
+ }
1048
1086
  if (plugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, plugins, skills, installPath);
1049
1087
  }
1050
1088
  this.state.setStatus(integrationId, "active");
@@ -1143,6 +1181,15 @@ var IntegrationManager = class {
1143
1181
  this.log.warn(`Failed to remove config for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1144
1182
  }
1145
1183
  }
1184
+ for (const [runtimeName, applier] of this.runtimeAppliers) {
1185
+ if (!await applier.isAvailable()) continue;
1186
+ this.log.info(`Removing MCP servers for ${integrationId} from ${runtimeName}`);
1187
+ try {
1188
+ await applier.removeMcpServers(integrationId);
1189
+ } catch (err) {
1190
+ this.log.warn(`Failed to remove MCP servers for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1191
+ }
1192
+ }
1146
1193
  this.state.setStatus(integrationId, "configured");
1147
1194
  return {
1148
1195
  ok: true,
@@ -1323,11 +1370,11 @@ var IntegrationManager = class {
1323
1370
  const manifestPath = join(installPath, "alfe-integration.yaml");
1324
1371
  if (!existsSync(manifestPath)) continue;
1325
1372
  try {
1326
- const manifest = parseManifestFile(manifestPath);
1327
- if (manifest.commands.length === 0) continue;
1373
+ const commands = parseManifestFile(manifestPath).commands ?? [];
1374
+ if (commands.length === 0) continue;
1328
1375
  result.push({
1329
1376
  integrationId: entry.id,
1330
- commands: manifest.commands.map((cmd) => ({
1377
+ commands: commands.map((cmd) => ({
1331
1378
  name: cmd.name,
1332
1379
  handler: cmd.handler,
1333
1380
  resolvedPath: join(installPath, cmd.handler),
@@ -1456,8 +1503,21 @@ var OpenClawApplier = class {
1456
1503
  "install",
1457
1504
  pkg
1458
1505
  ];
1459
- if (pkg.startsWith("@alfe.ai/")) args.push("--dangerously-force-unsafe-install");
1460
- await execFileAsync("openclaw", args, { timeout: 6e4 });
1506
+ const useUnsafeFlag = pkg.startsWith("@alfe.ai/");
1507
+ if (useUnsafeFlag) args.push("--dangerously-force-unsafe-install");
1508
+ try {
1509
+ await execFileAsync("openclaw", args, { timeout: 6e4 });
1510
+ } catch (err) {
1511
+ const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
1512
+ if (useUnsafeFlag && errText.includes("unknown option")) {
1513
+ log.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
1514
+ await execFileAsync("openclaw", [
1515
+ "plugins",
1516
+ "install",
1517
+ pkg
1518
+ ], { timeout: 6e4 });
1519
+ } else throw err;
1520
+ }
1461
1521
  } catch (err) {
1462
1522
  await new Promise((r) => {
1463
1523
  setTimeout(r, 500);
@@ -1624,6 +1684,83 @@ var OpenClawApplier = class {
1624
1684
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1625
1685
  this.writeTracking(tracking);
1626
1686
  }
1687
+ /**
1688
+ * Configure MCP servers in OpenClaw via `openclaw config set --batch-json`.
1689
+ *
1690
+ * Each server is written as `mcp.servers.{integrationId}-{serverId}.*`.
1691
+ * Applied servers are tracked in the tracking file for clean removal.
1692
+ */
1693
+ async applyMcpServers(integrationId, servers) {
1694
+ if (servers.length === 0) return;
1695
+ const batch = [];
1696
+ const trackedKeys = [];
1697
+ for (const server of servers) {
1698
+ const prefix = `mcp.servers.${`${integrationId}-${server.id}`}`;
1699
+ batch.push({
1700
+ path: `${prefix}.command`,
1701
+ value: server.command
1702
+ });
1703
+ if (server.args && server.args.length > 0) batch.push({
1704
+ path: `${prefix}.args`,
1705
+ value: server.args
1706
+ });
1707
+ if (server.env) for (const [envKey, envVal] of Object.entries(server.env)) batch.push({
1708
+ path: `${prefix}.env.${envKey}`,
1709
+ value: envVal
1710
+ });
1711
+ if (server.cwd) batch.push({
1712
+ path: `${prefix}.cwd`,
1713
+ value: server.cwd
1714
+ });
1715
+ trackedKeys.push(prefix);
1716
+ }
1717
+ try {
1718
+ await execFileAsync("openclaw", [
1719
+ "config",
1720
+ "set",
1721
+ "--batch-json",
1722
+ JSON.stringify(batch),
1723
+ "--strict-json"
1724
+ ], { timeout: 1e4 });
1725
+ } catch (err) {
1726
+ log.error({
1727
+ err: err instanceof Error ? err.message : String(err),
1728
+ batch
1729
+ }, "Failed to set MCP server config via openclaw config set --batch-json");
1730
+ throw err;
1731
+ }
1732
+ const tracking = this.readTracking();
1733
+ const mcpTracking = tracking._mcpServers ?? {};
1734
+ mcpTracking[integrationId] = trackedKeys;
1735
+ tracking._mcpServers = mcpTracking;
1736
+ this.writeTracking(tracking);
1737
+ }
1738
+ /**
1739
+ * Remove MCP servers previously applied by an integration.
1740
+ *
1741
+ * Reads the tracking file to find which `mcp.servers.*` keys this integration set,
1742
+ * then removes them via `openclaw config unset`.
1743
+ */
1744
+ async removeMcpServers(integrationId) {
1745
+ const tracking = this.readTracking();
1746
+ const mcpTracking = tracking._mcpServers ?? {};
1747
+ if (!(integrationId in mcpTracking)) return;
1748
+ const prefixes = mcpTracking[integrationId];
1749
+ for (const prefix of prefixes) try {
1750
+ await execFileAsync("openclaw", [
1751
+ "config",
1752
+ "unset",
1753
+ prefix
1754
+ ], { timeout: 1e4 });
1755
+ } catch (err) {
1756
+ log.warn({
1757
+ err: err instanceof Error ? err.message : String(err),
1758
+ prefix
1759
+ }, "Failed to unset MCP server config via openclaw config unset");
1760
+ }
1761
+ tracking._mcpServers = Object.fromEntries(Object.entries(mcpTracking).filter(([key]) => key !== integrationId));
1762
+ this.writeTracking(tracking);
1763
+ }
1627
1764
  isAvailable() {
1628
1765
  return Promise.resolve(existsSync(this.workspace));
1629
1766
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",