@alfe.ai/integrations 0.0.33 → 0.1.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +73 -196
  2. package/dist/index.js +154 -134
  3. package/package.json +3 -2
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
+ import { IntegrationManifest, IntegrationStateEntry, IntegrationStatus, IntegrationsStateFile, McpServerDeclaration, PluginInstall, SkillInstall } from "@alfe.ai/integration-manifest";
2
+ import { Manager } from "@alfe.ai/mcp-bundler";
3
+
1
4
  //#region src/registry.d.ts
5
+
2
6
  /**
3
7
  * Registry — fetches the integration registry index from the integrations service.
4
8
  *
@@ -216,187 +220,16 @@ declare class Installer {
216
220
  isInstalled(name: string): boolean;
217
221
  }
218
222
  //#endregion
219
- //#region ../integration-manifest/dist/index.d.ts
220
- //#region src/types.d.ts
221
-
222
- /**
223
- * TypeScript types for the Alfe integration manifest (`alfe-integration.yaml`).
224
- *
225
- * These are the canonical types used by all packages that interact with
226
- * integration manifests -- registry, lifecycle manager, CLI, etc.
227
- */
228
- type ConfigFieldType = 'secret' | 'string' | 'number' | 'boolean' | 'enum' | 'select' | 'multi_select' | 'oauth_connect';
229
- interface SelectOption {
230
- value: string;
231
- label: string;
232
- }
233
- interface ConfigSchemaField {
234
- key: string;
235
- type: ConfigFieldType;
236
- label: string;
237
- description?: string;
238
- required: boolean;
239
- default?: string | number | boolean;
240
- /** Who can mutate this field at runtime. Default: 'admin' */
241
- editable: 'admin' | 'agent';
242
- /** Only used when type === 'enum' */
243
- options?: string[];
244
- /** Structured options for select/multi_select fields */
245
- select_options?: SelectOption[];
246
- /** OAuth provider identifier for oauth_connect fields */
247
- oauth_provider?: string;
248
- /** Force specific scope groups for OAuth (e.g. ["chat"]) — hides other scopes in the dashboard */
249
- oauth_scopes?: string[];
250
- /** Integration ID to patch on OAuth callback (when using a shared OAuth provider) */
251
- oauth_integration_id?: string;
252
- /** If true, this field is not shown in the dashboard UI (install wizard or configure modal) */
253
- hidden?: boolean;
254
- /** Only show this field if another field has a truthy value (string) or matches a specific value (object) */
255
- depends_on_field?: string | {
256
- key: string;
257
- value: string | number | boolean;
258
- };
259
- /** Only show this field if a specific integration is installed */
260
- depends_on_integration?: string;
261
- }
262
- interface McpServerDeclaration {
263
- /** Unique identifier within this integration */
264
- id: string;
265
- /** Command to spawn (e.g., 'npx', 'node', 'xero-mcp-proxy') */
266
- command: string;
267
- /** Arguments for the command */
268
- args?: string[];
269
- /** Environment variables — supports {{config.KEY}} interpolation from config + secrets */
270
- env?: Record<string, string>;
271
- /** Working directory (optional) */
272
- cwd?: string;
273
- /** If true, applier skips auto-application — a lifecycle hook manages this MCP server instead */
274
- hook_managed?: boolean;
275
- }
276
- interface CommandDeclaration {
277
- /** Dot-namespaced command name (e.g. "support.diagnostic") */
278
- name: string;
279
- /** Relative path to handler file within integration directory */
280
- handler: string;
281
- /** Exported function name (default: "handle") */
282
- method?: string;
283
- /** Timeout in milliseconds (default: 30000) */
284
- timeout_ms?: number;
285
- /** Human-readable description */
286
- description?: string;
287
- }
288
- interface SkillInstall {
289
- /** Relative path within the integration repo to the skill directory */
290
- path?: string;
291
- /** ClawHub skill slug to install from the registry */
292
- clawhub?: string;
293
- }
294
- interface PluginInstall {
295
- /** npm package name to install */
296
- package: string;
297
- }
298
- type AgentRuntime = 'openclaw' | 'nanoclaw' | (string & {});
299
- interface RuntimeInstall {
300
- plugins?: PluginInstall[];
301
- skills?: SkillInstall[];
302
- /** Deep-merged into the runtime's agent config on activation */
303
- config?: Record<string, unknown>;
304
- }
305
- interface InstallTargets {
306
- /** Universal skills — applied to all runtimes */
307
- skills?: SkillInstall[];
308
- /** Universal plugins — applied to all runtimes */
309
- plugins?: PluginInstall[];
310
- /** Per-runtime installs */
311
- runtimes?: Record<AgentRuntime, RuntimeInstall>;
312
- }
313
- interface IntegrationHooks {
314
- pre_install?: string;
315
- post_install?: string;
316
- post_activate?: string;
317
- pre_uninstall?: string;
318
- post_uninstall?: string;
319
- health_check?: string;
320
- }
321
- interface IntegrationPricingPlan {
322
- name: string;
323
- price: number;
324
- currency?: string;
325
- interval?: 'month' | 'year';
326
- }
327
- interface IntegrationPricing {
328
- type: 'free' | 'paid' | 'usage';
329
- /** Single price (shorthand for integrations with one plan) */
330
- price?: number;
331
- currency?: string;
332
- interval?: 'month' | 'year';
333
- /** Description of usage-based pricing (for type: 'usage') */
334
- description?: string;
335
- /** Multiple plans/tiers (e.g., starter, growth, scale) */
336
- plans?: Record<string, IntegrationPricingPlan>;
337
- }
338
- interface IntegrationAuthor {
339
- name: string;
340
- url?: string;
341
- }
342
- interface IntegrationManifest {
343
- id: string;
344
- name: string;
345
- version: string;
346
- description: string;
347
- /** Simple author string (legacy) or structured author object */
348
- author: string | IntegrationAuthor;
349
- license: string;
350
- depends_on: string[];
351
- min_gateway_version: string;
352
- installs: InstallTargets;
353
- config_schema: ConfigSchemaField[];
354
- capabilities: string[];
355
- hooks: IntegrationHooks;
356
- commands: CommandDeclaration[];
357
- /** MCP servers to configure in the agent runtime */
358
- mcp_servers: McpServerDeclaration[];
359
- /** Git repository URL (HTTPS) — not present in YAML, injected by the publish API */
360
- repository?: string;
361
- /**
362
- * Agent runtimes this integration supports.
363
- * If omitted or empty, the integration is considered universal (all runtimes).
364
- * Example: ['openclaw'] means this integration only works with OpenClaw.
365
- */
366
- supported_agents?: AgentRuntime[];
367
- /**
368
- * Scopes where this integration can be installed.
369
- * Default: ['agent'] (per-agent only).
370
- * 'org' means it can be installed at the org level and cascades to all agents.
371
- */
372
- supported_scopes?: ('agent' | 'org')[];
373
- /** Marketplace metadata */
374
- publisherId?: string;
375
- icon?: string;
376
- pricing?: IntegrationPricing;
377
- preview_images?: string[];
378
- /** Long-form features list for the detail view */
379
- features?: string[];
380
- }
381
- type IntegrationStatus = 'installing' | 'installed' | 'configured' | 'active' | 'error';
382
- interface IntegrationStateEntry {
383
- status: IntegrationStatus;
384
- version: string;
385
- installedAt: string;
386
- config: Record<string, unknown>;
387
- error?: string;
388
- /** Number of consecutive auto-reinstall attempts. Reset on success or explicit reinstall. */
389
- reinstallAttempts?: number;
390
- }
391
- interface IntegrationsStateFile {
392
- version: number;
393
- integrations: Record<string, IntegrationStateEntry>;
394
- }
395
- //#endregion
396
223
  //#region src/runtime-applier.d.ts
397
224
  /**
398
225
  * RuntimeApplier — interface for applying/removing plugins and skills
399
226
  * to a specific agent runtime (OpenClaw, NanoClaw, etc.).
227
+ *
228
+ * Note: MCP server registration is NOT here — it moved to the
229
+ * runtime-agnostic `McpApplier` (`appliers/mcp-applier.ts`) which
230
+ * writes via the bundler manager + openclaw.json mirror. Removing
231
+ * the per-runtime `applyMcpServers` / `removeMcpServers` was part of
232
+ * the bundler-as-primitive cleanup (see `project_mcp_tool_lift_workstream`).
400
233
  */
401
234
  interface RuntimeApplier {
402
235
  /** The runtime identifier (e.g. 'openclaw', 'nanoclaw') */
@@ -419,14 +252,62 @@ interface RuntimeApplier {
419
252
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
420
253
  /** Remove config previously applied by an integration */
421
254
  removeConfig(integrationId: string): Promise<void>;
422
- /** Configure MCP servers in this runtime */
423
- applyMcpServers(integrationId: string, servers: McpServerDeclaration[]): Promise<void>;
424
- /** Remove MCP servers previously applied by an integration */
425
- removeMcpServers(integrationId: string): Promise<void>;
426
255
  /** Check if this runtime is available (e.g. workspace directory exists) */
427
256
  isAvailable(): Promise<boolean>;
428
257
  }
429
258
  //#endregion
259
+ //#region src/appliers/mcp-applier.d.ts
260
+ /**
261
+ * Minimal contract the applier needs from the agent API client.
262
+ * Wider than a single typed method so callers don't have to depend on
263
+ * the exact `AgentApiClient` shape — particularly handy for tests and
264
+ * for the eventual `getCredentials(provider)` generalisation.
265
+ */
266
+ interface CredentialsResolver {
267
+ getCredentials: (provider: string) => Promise<Record<string, unknown> | undefined>;
268
+ }
269
+ /**
270
+ * Daemon-level platform context exposed to `{{alfe.<key>}}` env
271
+ * interpolation. Reserved namespace so manifests can reference things
272
+ * like the agent's cloud apiUrl (for OAuth redirect URIs) without
273
+ * colliding with user-supplied `{{config.<key>}}`.
274
+ */
275
+ interface PlatformContext {
276
+ /** The agent's cloud apiUrl (e.g. https://api.alfe.ai). */
277
+ apiUrl?: string;
278
+ }
279
+ interface McpApplierOptions {
280
+ manager: Manager;
281
+ credentials: CredentialsResolver;
282
+ /** Optional — when omitted, `{{alfe.X}}` references are left as-is. */
283
+ platform?: PlatformContext;
284
+ }
285
+ declare class McpApplier {
286
+ private readonly manager;
287
+ private readonly credentials;
288
+ private readonly platform;
289
+ constructor(opts: McpApplierOptions);
290
+ /**
291
+ * Register every server declared by an integration. Idempotent —
292
+ * re-running for the same integration overwrites entries in place
293
+ * (the manager preserves `addedAt`). Returns the list of ids that
294
+ * actually landed in the store.
295
+ *
296
+ * Store writes are durable before each `manager.addServer` resolves,
297
+ * so the caller can ack the activation as soon as this returns —
298
+ * the daemon-hosted bundler will pick the change up via the store
299
+ * watcher and reconcile its children.
300
+ */
301
+ applyForIntegration(integrationId: string, servers: McpServerDeclaration[], mergedConfig: Record<string, unknown>): Promise<string[]>;
302
+ /**
303
+ * Drop every entry owned by this integration. Store writes are
304
+ * durable before `removeServersByOwner` resolves; the daemon's
305
+ * bundler reconciles via the store watcher.
306
+ */
307
+ removeForIntegration(integrationId: string): Promise<string[]>;
308
+ private resolveEnv;
309
+ }
310
+ //#endregion
430
311
  //#region src/types.d.ts
431
312
  /**
432
313
  * Integration command parameter types.
@@ -462,6 +343,15 @@ interface IntegrationManagerOptions {
462
343
  lockPath?: string;
463
344
  /** Fetcher for the integration registry — should be wired to api-client */
464
345
  registryFetcher: RegistryFetcher;
346
+ /**
347
+ * Runtime-agnostic MCP server applier. The manager routes
348
+ * `mcp_servers` declarations through it once per integration (the
349
+ * applier writes to the bundler store + openclaw.json mirror).
350
+ * Required — there is no fallback path; integrations with
351
+ * `mcp_servers` will skip MCP registration silently if this is
352
+ * omitted (the warn log makes this visible).
353
+ */
354
+ mcpApplier?: McpApplier;
465
355
  }
466
356
  interface ManagerResponse {
467
357
  ok: boolean;
@@ -496,6 +386,7 @@ declare class IntegrationManager {
496
386
  private installer;
497
387
  private runtimeAppliers;
498
388
  private lockManager;
389
+ private mcpApplier;
499
390
  /** In-memory secret store — NEVER persisted to disk */
500
391
  private secrets;
501
392
  constructor(options: IntegrationManagerOptions);
@@ -825,20 +716,6 @@ declare class OpenClawApplier implements RuntimeApplier {
825
716
  * then removes them via `openclaw config unset`.
826
717
  */
827
718
  removeConfig(integrationId: string): Promise<void>;
828
- /**
829
- * Configure MCP servers in OpenClaw via `openclaw config set --batch-json`.
830
- *
831
- * Each server is written as `mcp.servers.{integrationId}-{serverId}.*`.
832
- * Applied servers are tracked in the tracking file for clean removal.
833
- */
834
- applyMcpServers(integrationId: string, servers: McpServerDeclaration[]): Promise<void>;
835
- /**
836
- * Remove MCP servers previously applied by an integration.
837
- *
838
- * Reads the tracking file to find which `mcp.servers.*` keys this integration set,
839
- * then removes them via `openclaw config unset`.
840
- */
841
- removeMcpServers(integrationId: string): Promise<void>;
842
719
  isAvailable(): Promise<boolean>;
843
720
  private readTracking;
844
721
  private writeTracking;
@@ -954,4 +831,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
954
831
  resetReinstallAttempts(integrationId: string): void;
955
832
  }
956
833
  //#endregion
957
- export { type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, LockManager, OpenClawApplier, type OpenClawApplierOptions, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, RegistryResolveError, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
834
+ export { type CredentialsResolver, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, LockManager, McpApplier, type McpApplierOptions, OpenClawApplier, type OpenClawApplierOptions, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, RegistryResolveError, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
package/dist/index.js CHANGED
@@ -130,7 +130,7 @@ var Resolver = class {
130
130
  * can resolve them via Node's upward module resolution.
131
131
  */
132
132
  const execFileAsync$1 = promisify(execFile);
133
- const log$1 = createLogger("Installer");
133
+ const log$2 = createLogger("Installer");
134
134
  const INTEGRATIONS_DIR = join(homedir(), ".alfe", "integrations");
135
135
  const GIT_TIMEOUT_MS = 6e4;
136
136
  const NPM_TIMEOUT_MS = 6e4;
@@ -292,7 +292,7 @@ var Installer = class {
292
292
  dependencies: { ...SHARED_PACKAGES }
293
293
  };
294
294
  writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf-8");
295
- log$1.info("Installing shared @alfe.ai packages for integration hooks");
295
+ log$2.info("Installing shared @alfe.ai packages for integration hooks");
296
296
  await this.runNpmInstall(this.basePath);
297
297
  this.sharedPackagesReady = true;
298
298
  }
@@ -302,7 +302,7 @@ var Installer = class {
302
302
  */
303
303
  async installLocalDependencies(installPath) {
304
304
  if (!existsSync(join(installPath, "package.json"))) return;
305
- log$1.info({ path: installPath }, "Installing integration-specific npm dependencies");
305
+ log$2.info({ path: installPath }, "Installing integration-specific npm dependencies");
306
306
  await this.runNpmInstall(installPath);
307
307
  }
308
308
  async runNpmInstall(cwd) {
@@ -837,25 +837,6 @@ function interpolateSelfConfig(obj, config) {
837
837
  return result;
838
838
  }
839
839
  /**
840
- * Interpolate `{{config.KEY}}` patterns in MCP server env vars using
841
- * the merged config + secrets dict. Returns new declarations with
842
- * interpolated env values; skips hook_managed servers.
843
- */
844
- function interpolateMcpServerEnvs(servers, mergedConfig) {
845
- return servers.filter((s) => !s.hook_managed).map((server) => {
846
- if (!server.env) return server;
847
- const interpolatedEnv = {};
848
- for (const [key, value] of Object.entries(server.env)) interpolatedEnv[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
849
- const val = mergedConfig[configKey];
850
- return typeof val === "string" || typeof val === "number" ? String(val) : _match;
851
- });
852
- return {
853
- ...server,
854
- env: interpolatedEnv
855
- };
856
- });
857
- }
858
- /**
859
840
  * Merge universal installs with runtime-specific installs from the manifest.
860
841
  */
861
842
  function resolveInstallsForRuntime(manifest, runtime) {
@@ -875,6 +856,7 @@ var IntegrationManager = class {
875
856
  installer;
876
857
  runtimeAppliers;
877
858
  lockManager;
859
+ mcpApplier;
878
860
  /** In-memory secret store — NEVER persisted to disk */
879
861
  secrets = /* @__PURE__ */ new Map();
880
862
  constructor(options) {
@@ -884,6 +866,7 @@ var IntegrationManager = class {
884
866
  this.installer = new Installer(options.integrationsDir);
885
867
  this.runtimeAppliers = options.runtimeAppliers ?? /* @__PURE__ */ new Map();
886
868
  this.lockManager = new LockManager(options.lockPath);
869
+ this.mcpApplier = options.mcpApplier;
887
870
  }
888
871
  /**
889
872
  * Install an integration from the registry.
@@ -1102,22 +1085,20 @@ var IntegrationManager = class {
1102
1085
  await applier.applyConfig(integrationId, interpolatedConfig);
1103
1086
  configApplied = true;
1104
1087
  }
1105
- const mcpServers = manifest.mcp_servers ?? [];
1106
- if (mcpServers.length > 0) {
1107
- const secretEntries = this.secrets.get(integrationId);
1108
- const interpolatedServers = interpolateMcpServerEnvs(mcpServers, {
1109
- ...entry.config,
1110
- ...secretEntries ? Object.fromEntries(secretEntries) : {}
1111
- });
1112
- if (interpolatedServers.length > 0) {
1113
- this.log.info(`Applying ${String(interpolatedServers.length)} MCP server(s) for ${integrationId} to ${runtimeName}`);
1114
- await applier.applyMcpServers(integrationId, interpolatedServers);
1115
- configApplied = true;
1116
- }
1117
- }
1118
1088
  const appliedPlugins = plugins.filter((p) => !pluginFailures.includes(p.package));
1119
1089
  if (appliedPlugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, appliedPlugins, skills, installPath);
1120
1090
  }
1091
+ const mcpServers = manifest.mcp_servers ?? [];
1092
+ if (mcpServers.length > 0) if (!this.mcpApplier) this.log.warn(`Integration "${integrationId}" declares ${String(mcpServers.length)} mcp_server(s) but no mcpApplier is wired — skipping MCP registration`);
1093
+ else {
1094
+ const secretEntries = this.secrets.get(integrationId);
1095
+ const mergedConfig = {
1096
+ ...entry.config,
1097
+ ...secretEntries ? Object.fromEntries(secretEntries) : {}
1098
+ };
1099
+ this.log.info(`Applying ${String(mcpServers.length)} MCP server(s) for ${integrationId} via bundler manager`);
1100
+ await this.mcpApplier.applyForIntegration(integrationId, mcpServers, mergedConfig);
1101
+ }
1121
1102
  this.state.setStatus(integrationId, "active");
1122
1103
  if (manifest.hooks.post_activate) {
1123
1104
  this.log.info(`Running post_activate hook: ${manifest.hooks.post_activate}`);
@@ -1215,13 +1196,12 @@ var IntegrationManager = class {
1215
1196
  this.log.warn(`Failed to remove config for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1216
1197
  }
1217
1198
  }
1218
- for (const [runtimeName, applier] of this.runtimeAppliers) {
1219
- if (!await applier.isAvailable()) continue;
1220
- this.log.info(`Removing MCP servers for ${integrationId} from ${runtimeName}`);
1199
+ if (this.mcpApplier) {
1200
+ this.log.info(`Removing MCP servers for ${integrationId} via bundler manager`);
1221
1201
  try {
1222
- await applier.removeMcpServers(integrationId);
1202
+ await this.mcpApplier.removeForIntegration(integrationId);
1223
1203
  } catch (err) {
1224
- this.log.warn(`Failed to remove MCP servers for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1204
+ this.log.warn(`Failed to remove MCP servers for ${integrationId} via bundler manager: ${err instanceof Error ? err.message : String(err)}`);
1225
1205
  }
1226
1206
  }
1227
1207
  this.state.setStatus(integrationId, "configured");
@@ -1521,7 +1501,7 @@ var IntegrationManager = class {
1521
1501
  * is stored in a separate tracking file (config.json) for clean removal.
1522
1502
  */
1523
1503
  const execFileAsync = promisify(execFile);
1524
- const log = createLogger("OpenClawApplier");
1504
+ const log$1 = createLogger("OpenClawApplier");
1525
1505
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
1526
1506
  function flattenConfig(obj, prefix = "") {
1527
1507
  const entries = [];
@@ -1599,11 +1579,11 @@ var OpenClawApplier = class {
1599
1579
  await this.ensurePluginsAllow(pkg);
1600
1580
  this.cleanupUntrackedExtensionInstall(pkg);
1601
1581
  if (opts?.force && this.isPluginInstalled(pkg)) {
1602
- log.info({ pkg }, "Force mode — uninstalling plugin before reinstall");
1582
+ log$1.info({ pkg }, "Force mode — uninstalling plugin before reinstall");
1603
1583
  try {
1604
1584
  await this.removePlugin(pkg);
1605
1585
  } catch (err) {
1606
- log.warn({
1586
+ log$1.warn({
1607
1587
  pkg,
1608
1588
  err: err instanceof Error ? err.message : String(err)
1609
1589
  }, "Failed to uninstall plugin during force reinstall — proceeding");
@@ -1624,7 +1604,7 @@ var OpenClawApplier = class {
1624
1604
  } catch (err) {
1625
1605
  const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
1626
1606
  if (useUnsafeFlag && errText.includes("unknown option")) {
1627
- log.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
1607
+ log$1.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
1628
1608
  await execFileAsync("openclaw", baseArgs, { timeout: 6e4 });
1629
1609
  } else throw err;
1630
1610
  }
@@ -1633,7 +1613,7 @@ var OpenClawApplier = class {
1633
1613
  setTimeout(r, 500);
1634
1614
  });
1635
1615
  if (!this.isPluginInstalled(pkg)) throw err;
1636
- log.warn({
1616
+ log$1.warn({
1637
1617
  pkg,
1638
1618
  err: err instanceof Error ? err.message : String(err)
1639
1619
  }, "openclaw plugins install exited with warnings but plugin is installed");
@@ -1668,7 +1648,7 @@ var OpenClawApplier = class {
1668
1648
  JSON.stringify(updated)
1669
1649
  ], { timeout: 1e4 });
1670
1650
  } catch (err) {
1671
- log.warn({
1651
+ log$1.warn({
1672
1652
  err: err instanceof Error ? err.message : String(err),
1673
1653
  pkg
1674
1654
  }, "Failed to set plugins.allow via openclaw config set");
@@ -1721,12 +1701,12 @@ var OpenClawApplier = class {
1721
1701
  recursive: true,
1722
1702
  force: true
1723
1703
  });
1724
- log.info({
1704
+ log$1.info({
1725
1705
  pkg,
1726
1706
  removed: fullPath
1727
1707
  }, "Removed untracked extensions/ install — will reinstall via npm path");
1728
1708
  } catch (err) {
1729
- log.warn({
1709
+ log$1.warn({
1730
1710
  pkg,
1731
1711
  removed: fullPath,
1732
1712
  err: err instanceof Error ? err.message : String(err)
@@ -1749,21 +1729,21 @@ var OpenClawApplier = class {
1749
1729
  return Promise.resolve();
1750
1730
  }
1751
1731
  async applyClawHubSkill(slug) {
1752
- log.info({ slug }, "Installing skill from ClawHub");
1732
+ log$1.info({ slug }, "Installing skill from ClawHub");
1753
1733
  try {
1754
1734
  await execFileAsync("openclaw", [
1755
1735
  "skills",
1756
1736
  "install",
1757
1737
  slug
1758
1738
  ], { timeout: 6e4 });
1759
- log.info({ slug }, "ClawHub skill installed");
1739
+ log$1.info({ slug }, "ClawHub skill installed");
1760
1740
  } catch (err) {
1761
1741
  const msg = err instanceof Error ? err.message : String(err);
1762
1742
  if (msg.includes("already exists") || msg.includes("Skill already exists")) {
1763
- log.info({ slug }, "ClawHub skill already installed (skipped)");
1743
+ log$1.info({ slug }, "ClawHub skill already installed (skipped)");
1764
1744
  return;
1765
1745
  }
1766
- log.error({
1746
+ log$1.error({
1767
1747
  slug,
1768
1748
  err: msg
1769
1749
  }, "ClawHub skill install failed");
@@ -1776,7 +1756,7 @@ var OpenClawApplier = class {
1776
1756
  recursive: true,
1777
1757
  force: true
1778
1758
  });
1779
- log.info({ slug }, "ClawHub skill removed");
1759
+ log$1.info({ slug }, "ClawHub skill removed");
1780
1760
  }
1781
1761
  return Promise.resolve();
1782
1762
  }
@@ -1812,7 +1792,7 @@ var OpenClawApplier = class {
1812
1792
  JSON.stringify(merged)
1813
1793
  ], { timeout: 1e4 });
1814
1794
  } catch (err) {
1815
- log.error({
1795
+ log$1.error({
1816
1796
  err: err instanceof Error ? err.message : String(err),
1817
1797
  parentPath
1818
1798
  }, "Failed to set config subtree via openclaw config set");
@@ -1827,7 +1807,7 @@ var OpenClawApplier = class {
1827
1807
  JSON.stringify(leaves)
1828
1808
  ], { timeout: 1e4 });
1829
1809
  } catch (err) {
1830
- log.error({
1810
+ log$1.error({
1831
1811
  err: err instanceof Error ? err.message : String(err),
1832
1812
  batch: leaves
1833
1813
  }, "Failed to set config via openclaw config set --batch-json");
@@ -1853,7 +1833,7 @@ var OpenClawApplier = class {
1853
1833
  path
1854
1834
  ], { timeout: 1e4 });
1855
1835
  } catch (err) {
1856
- log.warn({
1836
+ log$1.warn({
1857
1837
  err: err instanceof Error ? err.message : String(err),
1858
1838
  path
1859
1839
  }, "Failed to unset config via openclaw config unset");
@@ -1875,7 +1855,7 @@ var OpenClawApplier = class {
1875
1855
  JSON.stringify(remaining)
1876
1856
  ], { timeout: 1e4 });
1877
1857
  } catch (err) {
1878
- log.warn({
1858
+ log$1.warn({
1879
1859
  err: err instanceof Error ? err.message : String(err),
1880
1860
  parentPath
1881
1861
  }, "Failed to update parent config during remove");
@@ -1884,82 +1864,6 @@ var OpenClawApplier = class {
1884
1864
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1885
1865
  this.writeTracking(tracking);
1886
1866
  }
1887
- /**
1888
- * Configure MCP servers in OpenClaw via `openclaw config set --batch-json`.
1889
- *
1890
- * Each server is written as `mcp.servers.{integrationId}-{serverId}.*`.
1891
- * Applied servers are tracked in the tracking file for clean removal.
1892
- */
1893
- async applyMcpServers(integrationId, servers) {
1894
- if (servers.length === 0) return;
1895
- const batch = [];
1896
- const trackedKeys = [];
1897
- for (const server of servers) {
1898
- const prefix = `mcp.servers.${`${integrationId}-${server.id}`}`;
1899
- batch.push({
1900
- path: `${prefix}.command`,
1901
- value: server.command
1902
- });
1903
- if (server.args && server.args.length > 0) batch.push({
1904
- path: `${prefix}.args`,
1905
- value: server.args
1906
- });
1907
- if (server.env) for (const [envKey, envVal] of Object.entries(server.env)) batch.push({
1908
- path: `${prefix}.env.${envKey}`,
1909
- value: envVal
1910
- });
1911
- if (server.cwd) batch.push({
1912
- path: `${prefix}.cwd`,
1913
- value: server.cwd
1914
- });
1915
- trackedKeys.push(prefix);
1916
- }
1917
- try {
1918
- await execFileAsync("openclaw", [
1919
- "config",
1920
- "set",
1921
- "--batch-json",
1922
- JSON.stringify(batch)
1923
- ], { timeout: 1e4 });
1924
- } catch (err) {
1925
- log.error({
1926
- err: err instanceof Error ? err.message : String(err),
1927
- batch
1928
- }, "Failed to set MCP server config via openclaw config set --batch-json");
1929
- throw err;
1930
- }
1931
- const tracking = this.readTracking();
1932
- const mcpTracking = tracking._mcpServers ?? {};
1933
- mcpTracking[integrationId] = trackedKeys;
1934
- tracking._mcpServers = mcpTracking;
1935
- this.writeTracking(tracking);
1936
- }
1937
- /**
1938
- * Remove MCP servers previously applied by an integration.
1939
- *
1940
- * Reads the tracking file to find which `mcp.servers.*` keys this integration set,
1941
- * then removes them via `openclaw config unset`.
1942
- */
1943
- async removeMcpServers(integrationId) {
1944
- const tracking = this.readTracking();
1945
- const mcpTracking = tracking._mcpServers ?? {};
1946
- if (!(integrationId in mcpTracking)) return;
1947
- const prefixes = mcpTracking[integrationId];
1948
- for (const prefix of prefixes) try {
1949
- await execFileAsync("openclaw", [
1950
- "config",
1951
- "unset",
1952
- prefix
1953
- ], { timeout: 1e4 });
1954
- } catch (err) {
1955
- log.warn({
1956
- err: err instanceof Error ? err.message : String(err),
1957
- prefix
1958
- }, "Failed to unset MCP server config via openclaw config unset");
1959
- }
1960
- tracking._mcpServers = Object.fromEntries(Object.entries(mcpTracking).filter(([key]) => key !== integrationId));
1961
- this.writeTracking(tracking);
1962
- }
1963
1867
  isAvailable() {
1964
1868
  return Promise.resolve(existsSync(this.home));
1965
1869
  }
@@ -1977,6 +1881,122 @@ var OpenClawApplier = class {
1977
1881
  }
1978
1882
  };
1979
1883
  //#endregion
1884
+ //#region src/appliers/mcp-applier.ts
1885
+ const log = createLogger("McpApplier");
1886
+ const CONFIG_TEMPLATE_RE = /\{\{config\.([a-zA-Z0-9_]+)\}\}/g;
1887
+ const CREDENTIALS_TEMPLATE_RE = /\{\{credentials\.([a-z0-9-]+)\.([a-zA-Z0-9_]+)\}\}/g;
1888
+ const ALFE_TEMPLATE_RE = /\{\{alfe\.([a-zA-Z0-9_]+)\}\}/g;
1889
+ var McpApplier = class {
1890
+ manager;
1891
+ credentials;
1892
+ platform;
1893
+ constructor(opts) {
1894
+ this.manager = opts.manager;
1895
+ this.credentials = opts.credentials;
1896
+ this.platform = opts.platform ?? {};
1897
+ }
1898
+ /**
1899
+ * Register every server declared by an integration. Idempotent —
1900
+ * re-running for the same integration overwrites entries in place
1901
+ * (the manager preserves `addedAt`). Returns the list of ids that
1902
+ * actually landed in the store.
1903
+ *
1904
+ * Store writes are durable before each `manager.addServer` resolves,
1905
+ * so the caller can ack the activation as soon as this returns —
1906
+ * the daemon-hosted bundler will pick the change up via the store
1907
+ * watcher and reconcile its children.
1908
+ */
1909
+ async applyForIntegration(integrationId, servers, mergedConfig) {
1910
+ const owner = `integration:${integrationId}`;
1911
+ const applied = [];
1912
+ for (const server of servers) {
1913
+ const id = `${integrationId}-${server.id}`;
1914
+ const envResolved = await this.resolveEnv(server, mergedConfig);
1915
+ if (envResolved == null) {
1916
+ log.info({
1917
+ integrationId,
1918
+ server: server.id,
1919
+ provider: server.requires_credentials
1920
+ }, "Skipping MCP server registration — required credentials missing or fetch failed");
1921
+ continue;
1922
+ }
1923
+ await this.manager.addServer(toBundlerConfig(server, envResolved), {
1924
+ id,
1925
+ owner
1926
+ });
1927
+ applied.push(id);
1928
+ }
1929
+ return applied;
1930
+ }
1931
+ /**
1932
+ * Drop every entry owned by this integration. Store writes are
1933
+ * durable before `removeServersByOwner` resolves; the daemon's
1934
+ * bundler reconciles via the store watcher.
1935
+ */
1936
+ async removeForIntegration(integrationId) {
1937
+ const owner = `integration:${integrationId}`;
1938
+ return this.manager.removeServersByOwner(owner);
1939
+ }
1940
+ async resolveEnv(server, mergedConfig) {
1941
+ if (!server.env || Object.keys(server.env).length === 0) return {};
1942
+ const needsCredentials = server.requires_credentials;
1943
+ let credentialsCache;
1944
+ if (needsCredentials) {
1945
+ try {
1946
+ credentialsCache = await this.credentials.getCredentials(needsCredentials);
1947
+ } catch (err) {
1948
+ log.warn({
1949
+ err: errMsg(err),
1950
+ provider: needsCredentials
1951
+ }, "Credentials fetch threw — skipping MCP registration");
1952
+ return null;
1953
+ }
1954
+ if (!credentialsCache || Object.keys(credentialsCache).length === 0) return null;
1955
+ }
1956
+ const resolved = {};
1957
+ for (const [key, value] of Object.entries(server.env)) {
1958
+ const interpolated = interpolateString(value, mergedConfig, credentialsCache, this.platform);
1959
+ if (needsCredentials && hasCredentialsPlaceholder(interpolated)) {
1960
+ log.warn({
1961
+ provider: needsCredentials,
1962
+ key
1963
+ }, "Credentials interpolation left a placeholder — skipping MCP registration");
1964
+ return null;
1965
+ }
1966
+ resolved[key] = interpolated;
1967
+ }
1968
+ return resolved;
1969
+ }
1970
+ };
1971
+ function hasCredentialsPlaceholder(value) {
1972
+ return /\{\{credentials\.[a-z0-9-]+\.[a-zA-Z0-9_]+\}\}/.test(value);
1973
+ }
1974
+ function interpolateString(value, mergedConfig, credentials, platform) {
1975
+ let next = value.replace(CONFIG_TEMPLATE_RE, (match, configKey) => {
1976
+ const v = mergedConfig[configKey];
1977
+ return typeof v === "string" || typeof v === "number" ? String(v) : match;
1978
+ });
1979
+ if (credentials) next = next.replace(CREDENTIALS_TEMPLATE_RE, (match, _provider, field) => {
1980
+ const v = credentials[field];
1981
+ return typeof v === "string" || typeof v === "number" ? String(v) : match;
1982
+ });
1983
+ next = next.replace(ALFE_TEMPLATE_RE, (match, platformKey) => {
1984
+ const v = platform[platformKey];
1985
+ return typeof v === "string" || typeof v === "number" ? String(v) : match;
1986
+ });
1987
+ return next;
1988
+ }
1989
+ function toBundlerConfig(server, resolvedEnv) {
1990
+ const cfg = { command: server.command };
1991
+ if (server.args && server.args.length > 0) cfg.args = server.args;
1992
+ if (Object.keys(resolvedEnv).length > 0) cfg.env = resolvedEnv;
1993
+ if (server.cwd) cfg.cwd = server.cwd;
1994
+ return cfg;
1995
+ }
1996
+ function errMsg(err) {
1997
+ return err instanceof Error ? err.message : String(err);
1998
+ }
1999
+ //#endregion
1980
2000
  //#region src/adapter.ts
1981
2001
  var IntegrationManagerAdapter = class {
1982
2002
  constructor(manager) {
@@ -2035,4 +2055,4 @@ var IntegrationManagerAdapter = class {
2035
2055
  }
2036
2056
  };
2037
2057
  //#endregion
2038
- export { Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
2058
+ export { Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.33",
3
+ "version": "0.1.1",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@auriclabs/logger": "^0.1.1",
16
- "@alfe.ai/integration-manifest": "^0.0.11"
16
+ "@alfe.ai/integration-manifest": "^0.1.0",
17
+ "@alfe.ai/mcp-bundler": "^0.1.1"
17
18
  },
18
19
  "files": [
19
20
  "dist"