@alfe.ai/integrations 0.0.32 → 0.1.0

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 +76 -196
  2. package/dist/index.js +179 -139
  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,65 @@ 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
+ * Awaits `manager.flush()` before returning so the openclaw.json
297
+ * mirror write has fired by the time the caller proceeds. Without
298
+ * this, an integration activation's success ack would race ahead of
299
+ * the runtime's openclaw.json update — the cloud would see
300
+ * `actual.status = active` while OpenClaw still has the stale MCP
301
+ * set in memory, violating the actual-status protocol contract.
302
+ */
303
+ applyForIntegration(integrationId: string, servers: McpServerDeclaration[], mergedConfig: Record<string, unknown>): Promise<string[]>;
304
+ /**
305
+ * Drop every entry owned by this integration. Like `applyForIntegration`,
306
+ * awaits the mirror flush so the runtime is signalled before the call
307
+ * resolves — the uninstall ack shouldn't outrun openclaw.json being
308
+ * cleaned up.
309
+ */
310
+ removeForIntegration(integrationId: string): Promise<string[]>;
311
+ private resolveEnv;
312
+ }
313
+ //#endregion
430
314
  //#region src/types.d.ts
431
315
  /**
432
316
  * Integration command parameter types.
@@ -462,6 +346,15 @@ interface IntegrationManagerOptions {
462
346
  lockPath?: string;
463
347
  /** Fetcher for the integration registry — should be wired to api-client */
464
348
  registryFetcher: RegistryFetcher;
349
+ /**
350
+ * Runtime-agnostic MCP server applier. The manager routes
351
+ * `mcp_servers` declarations through it once per integration (the
352
+ * applier writes to the bundler store + openclaw.json mirror).
353
+ * Required — there is no fallback path; integrations with
354
+ * `mcp_servers` will skip MCP registration silently if this is
355
+ * omitted (the warn log makes this visible).
356
+ */
357
+ mcpApplier?: McpApplier;
465
358
  }
466
359
  interface ManagerResponse {
467
360
  ok: boolean;
@@ -496,6 +389,7 @@ declare class IntegrationManager {
496
389
  private installer;
497
390
  private runtimeAppliers;
498
391
  private lockManager;
392
+ private mcpApplier;
499
393
  /** In-memory secret store — NEVER persisted to disk */
500
394
  private secrets;
501
395
  constructor(options: IntegrationManagerOptions);
@@ -825,20 +719,6 @@ declare class OpenClawApplier implements RuntimeApplier {
825
719
  * then removes them via `openclaw config unset`.
826
720
  */
827
721
  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
722
  isAvailable(): Promise<boolean>;
843
723
  private readTracking;
844
724
  private writeTracking;
@@ -954,4 +834,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
954
834
  resetReinstallAttempts(integrationId: string): void;
955
835
  }
956
836
  //#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 };
837
+ 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) {
@@ -811,35 +811,30 @@ async function runHookWithContext(integrationPath, hookScript, options) {
811
811
  * Deep-walk an object and replace `{{config.KEY}}` patterns with values
812
812
  * from the per-agent config. Used to compose manifest runtime config
813
813
  * with per-agent secrets (e.g., gateway token).
814
+ *
815
+ * Whole-value substitution: when a string is exactly `{{config.KEY}}` with
816
+ * no surrounding text, the raw config value is substituted preserving its
817
+ * type (array, object, boolean, …). For interpolation inside a larger
818
+ * string, only string/number values are substituted; other types fall
819
+ * through to keep the placeholder.
814
820
  */
821
+ const FULL_PLACEHOLDER = /^\{\{config\.([a-zA-Z0-9_]+)\}\}$/;
815
822
  function interpolateSelfConfig(obj, config) {
816
823
  const result = {};
817
- for (const [key, value] of Object.entries(obj)) if (typeof value === "string") result[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
818
- const val = config[configKey];
819
- return typeof val === "string" || typeof val === "number" ? String(val) : _match;
820
- });
821
- else if (value && typeof value === "object" && !Array.isArray(value)) result[key] = interpolateSelfConfig(value, config);
822
- else result[key] = value;
823
- return result;
824
- }
825
- /**
826
- * Interpolate `{{config.KEY}}` patterns in MCP server env vars using
827
- * the merged config + secrets dict. Returns new declarations with
828
- * interpolated env values; skips hook_managed servers.
829
- */
830
- function interpolateMcpServerEnvs(servers, mergedConfig) {
831
- return servers.filter((s) => !s.hook_managed).map((server) => {
832
- if (!server.env) return server;
833
- const interpolatedEnv = {};
834
- for (const [key, value] of Object.entries(server.env)) interpolatedEnv[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
835
- const val = mergedConfig[configKey];
824
+ for (const [key, value] of Object.entries(obj)) if (typeof value === "string") {
825
+ const wholeMatch = FULL_PLACEHOLDER.exec(value);
826
+ if (wholeMatch) {
827
+ const raw = config[wholeMatch[1]];
828
+ result[key] = raw !== void 0 ? raw : value;
829
+ continue;
830
+ }
831
+ result[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
832
+ const val = config[configKey];
836
833
  return typeof val === "string" || typeof val === "number" ? String(val) : _match;
837
834
  });
838
- return {
839
- ...server,
840
- env: interpolatedEnv
841
- };
842
- });
835
+ } else if (value && typeof value === "object" && !Array.isArray(value)) result[key] = interpolateSelfConfig(value, config);
836
+ else result[key] = value;
837
+ return result;
843
838
  }
844
839
  /**
845
840
  * Merge universal installs with runtime-specific installs from the manifest.
@@ -861,6 +856,7 @@ var IntegrationManager = class {
861
856
  installer;
862
857
  runtimeAppliers;
863
858
  lockManager;
859
+ mcpApplier;
864
860
  /** In-memory secret store — NEVER persisted to disk */
865
861
  secrets = /* @__PURE__ */ new Map();
866
862
  constructor(options) {
@@ -870,6 +866,7 @@ var IntegrationManager = class {
870
866
  this.installer = new Installer(options.integrationsDir);
871
867
  this.runtimeAppliers = options.runtimeAppliers ?? /* @__PURE__ */ new Map();
872
868
  this.lockManager = new LockManager(options.lockPath);
869
+ this.mcpApplier = options.mcpApplier;
873
870
  }
874
871
  /**
875
872
  * Install an integration from the registry.
@@ -1088,22 +1085,20 @@ var IntegrationManager = class {
1088
1085
  await applier.applyConfig(integrationId, interpolatedConfig);
1089
1086
  configApplied = true;
1090
1087
  }
1091
- const mcpServers = manifest.mcp_servers ?? [];
1092
- if (mcpServers.length > 0) {
1093
- const secretEntries = this.secrets.get(integrationId);
1094
- const interpolatedServers = interpolateMcpServerEnvs(mcpServers, {
1095
- ...entry.config,
1096
- ...secretEntries ? Object.fromEntries(secretEntries) : {}
1097
- });
1098
- if (interpolatedServers.length > 0) {
1099
- this.log.info(`Applying ${String(interpolatedServers.length)} MCP server(s) for ${integrationId} to ${runtimeName}`);
1100
- await applier.applyMcpServers(integrationId, interpolatedServers);
1101
- configApplied = true;
1102
- }
1103
- }
1104
1088
  const appliedPlugins = plugins.filter((p) => !pluginFailures.includes(p.package));
1105
1089
  if (appliedPlugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, appliedPlugins, skills, installPath);
1106
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
+ }
1107
1102
  this.state.setStatus(integrationId, "active");
1108
1103
  if (manifest.hooks.post_activate) {
1109
1104
  this.log.info(`Running post_activate hook: ${manifest.hooks.post_activate}`);
@@ -1201,13 +1196,12 @@ var IntegrationManager = class {
1201
1196
  this.log.warn(`Failed to remove config for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
1202
1197
  }
1203
1198
  }
1204
- for (const [runtimeName, applier] of this.runtimeAppliers) {
1205
- if (!await applier.isAvailable()) continue;
1206
- 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`);
1207
1201
  try {
1208
- await applier.removeMcpServers(integrationId);
1202
+ await this.mcpApplier.removeForIntegration(integrationId);
1209
1203
  } catch (err) {
1210
- 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)}`);
1211
1205
  }
1212
1206
  }
1213
1207
  this.state.setStatus(integrationId, "configured");
@@ -1507,7 +1501,7 @@ var IntegrationManager = class {
1507
1501
  * is stored in a separate tracking file (config.json) for clean removal.
1508
1502
  */
1509
1503
  const execFileAsync = promisify(execFile);
1510
- const log = createLogger("OpenClawApplier");
1504
+ const log$1 = createLogger("OpenClawApplier");
1511
1505
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
1512
1506
  function flattenConfig(obj, prefix = "") {
1513
1507
  const entries = [];
@@ -1585,11 +1579,11 @@ var OpenClawApplier = class {
1585
1579
  await this.ensurePluginsAllow(pkg);
1586
1580
  this.cleanupUntrackedExtensionInstall(pkg);
1587
1581
  if (opts?.force && this.isPluginInstalled(pkg)) {
1588
- log.info({ pkg }, "Force mode — uninstalling plugin before reinstall");
1582
+ log$1.info({ pkg }, "Force mode — uninstalling plugin before reinstall");
1589
1583
  try {
1590
1584
  await this.removePlugin(pkg);
1591
1585
  } catch (err) {
1592
- log.warn({
1586
+ log$1.warn({
1593
1587
  pkg,
1594
1588
  err: err instanceof Error ? err.message : String(err)
1595
1589
  }, "Failed to uninstall plugin during force reinstall — proceeding");
@@ -1610,7 +1604,7 @@ var OpenClawApplier = class {
1610
1604
  } catch (err) {
1611
1605
  const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
1612
1606
  if (useUnsafeFlag && errText.includes("unknown option")) {
1613
- 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");
1614
1608
  await execFileAsync("openclaw", baseArgs, { timeout: 6e4 });
1615
1609
  } else throw err;
1616
1610
  }
@@ -1619,7 +1613,7 @@ var OpenClawApplier = class {
1619
1613
  setTimeout(r, 500);
1620
1614
  });
1621
1615
  if (!this.isPluginInstalled(pkg)) throw err;
1622
- log.warn({
1616
+ log$1.warn({
1623
1617
  pkg,
1624
1618
  err: err instanceof Error ? err.message : String(err)
1625
1619
  }, "openclaw plugins install exited with warnings but plugin is installed");
@@ -1654,7 +1648,7 @@ var OpenClawApplier = class {
1654
1648
  JSON.stringify(updated)
1655
1649
  ], { timeout: 1e4 });
1656
1650
  } catch (err) {
1657
- log.warn({
1651
+ log$1.warn({
1658
1652
  err: err instanceof Error ? err.message : String(err),
1659
1653
  pkg
1660
1654
  }, "Failed to set plugins.allow via openclaw config set");
@@ -1707,12 +1701,12 @@ var OpenClawApplier = class {
1707
1701
  recursive: true,
1708
1702
  force: true
1709
1703
  });
1710
- log.info({
1704
+ log$1.info({
1711
1705
  pkg,
1712
1706
  removed: fullPath
1713
1707
  }, "Removed untracked extensions/ install — will reinstall via npm path");
1714
1708
  } catch (err) {
1715
- log.warn({
1709
+ log$1.warn({
1716
1710
  pkg,
1717
1711
  removed: fullPath,
1718
1712
  err: err instanceof Error ? err.message : String(err)
@@ -1735,21 +1729,21 @@ var OpenClawApplier = class {
1735
1729
  return Promise.resolve();
1736
1730
  }
1737
1731
  async applyClawHubSkill(slug) {
1738
- log.info({ slug }, "Installing skill from ClawHub");
1732
+ log$1.info({ slug }, "Installing skill from ClawHub");
1739
1733
  try {
1740
1734
  await execFileAsync("openclaw", [
1741
1735
  "skills",
1742
1736
  "install",
1743
1737
  slug
1744
1738
  ], { timeout: 6e4 });
1745
- log.info({ slug }, "ClawHub skill installed");
1739
+ log$1.info({ slug }, "ClawHub skill installed");
1746
1740
  } catch (err) {
1747
1741
  const msg = err instanceof Error ? err.message : String(err);
1748
1742
  if (msg.includes("already exists") || msg.includes("Skill already exists")) {
1749
- log.info({ slug }, "ClawHub skill already installed (skipped)");
1743
+ log$1.info({ slug }, "ClawHub skill already installed (skipped)");
1750
1744
  return;
1751
1745
  }
1752
- log.error({
1746
+ log$1.error({
1753
1747
  slug,
1754
1748
  err: msg
1755
1749
  }, "ClawHub skill install failed");
@@ -1762,7 +1756,7 @@ var OpenClawApplier = class {
1762
1756
  recursive: true,
1763
1757
  force: true
1764
1758
  });
1765
- log.info({ slug }, "ClawHub skill removed");
1759
+ log$1.info({ slug }, "ClawHub skill removed");
1766
1760
  }
1767
1761
  return Promise.resolve();
1768
1762
  }
@@ -1798,7 +1792,7 @@ var OpenClawApplier = class {
1798
1792
  JSON.stringify(merged)
1799
1793
  ], { timeout: 1e4 });
1800
1794
  } catch (err) {
1801
- log.error({
1795
+ log$1.error({
1802
1796
  err: err instanceof Error ? err.message : String(err),
1803
1797
  parentPath
1804
1798
  }, "Failed to set config subtree via openclaw config set");
@@ -1813,7 +1807,7 @@ var OpenClawApplier = class {
1813
1807
  JSON.stringify(leaves)
1814
1808
  ], { timeout: 1e4 });
1815
1809
  } catch (err) {
1816
- log.error({
1810
+ log$1.error({
1817
1811
  err: err instanceof Error ? err.message : String(err),
1818
1812
  batch: leaves
1819
1813
  }, "Failed to set config via openclaw config set --batch-json");
@@ -1839,7 +1833,7 @@ var OpenClawApplier = class {
1839
1833
  path
1840
1834
  ], { timeout: 1e4 });
1841
1835
  } catch (err) {
1842
- log.warn({
1836
+ log$1.warn({
1843
1837
  err: err instanceof Error ? err.message : String(err),
1844
1838
  path
1845
1839
  }, "Failed to unset config via openclaw config unset");
@@ -1861,7 +1855,7 @@ var OpenClawApplier = class {
1861
1855
  JSON.stringify(remaining)
1862
1856
  ], { timeout: 1e4 });
1863
1857
  } catch (err) {
1864
- log.warn({
1858
+ log$1.warn({
1865
1859
  err: err instanceof Error ? err.message : String(err),
1866
1860
  parentPath
1867
1861
  }, "Failed to update parent config during remove");
@@ -1870,82 +1864,6 @@ var OpenClawApplier = class {
1870
1864
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1871
1865
  this.writeTracking(tracking);
1872
1866
  }
1873
- /**
1874
- * Configure MCP servers in OpenClaw via `openclaw config set --batch-json`.
1875
- *
1876
- * Each server is written as `mcp.servers.{integrationId}-{serverId}.*`.
1877
- * Applied servers are tracked in the tracking file for clean removal.
1878
- */
1879
- async applyMcpServers(integrationId, servers) {
1880
- if (servers.length === 0) return;
1881
- const batch = [];
1882
- const trackedKeys = [];
1883
- for (const server of servers) {
1884
- const prefix = `mcp.servers.${`${integrationId}-${server.id}`}`;
1885
- batch.push({
1886
- path: `${prefix}.command`,
1887
- value: server.command
1888
- });
1889
- if (server.args && server.args.length > 0) batch.push({
1890
- path: `${prefix}.args`,
1891
- value: server.args
1892
- });
1893
- if (server.env) for (const [envKey, envVal] of Object.entries(server.env)) batch.push({
1894
- path: `${prefix}.env.${envKey}`,
1895
- value: envVal
1896
- });
1897
- if (server.cwd) batch.push({
1898
- path: `${prefix}.cwd`,
1899
- value: server.cwd
1900
- });
1901
- trackedKeys.push(prefix);
1902
- }
1903
- try {
1904
- await execFileAsync("openclaw", [
1905
- "config",
1906
- "set",
1907
- "--batch-json",
1908
- JSON.stringify(batch)
1909
- ], { timeout: 1e4 });
1910
- } catch (err) {
1911
- log.error({
1912
- err: err instanceof Error ? err.message : String(err),
1913
- batch
1914
- }, "Failed to set MCP server config via openclaw config set --batch-json");
1915
- throw err;
1916
- }
1917
- const tracking = this.readTracking();
1918
- const mcpTracking = tracking._mcpServers ?? {};
1919
- mcpTracking[integrationId] = trackedKeys;
1920
- tracking._mcpServers = mcpTracking;
1921
- this.writeTracking(tracking);
1922
- }
1923
- /**
1924
- * Remove MCP servers previously applied by an integration.
1925
- *
1926
- * Reads the tracking file to find which `mcp.servers.*` keys this integration set,
1927
- * then removes them via `openclaw config unset`.
1928
- */
1929
- async removeMcpServers(integrationId) {
1930
- const tracking = this.readTracking();
1931
- const mcpTracking = tracking._mcpServers ?? {};
1932
- if (!(integrationId in mcpTracking)) return;
1933
- const prefixes = mcpTracking[integrationId];
1934
- for (const prefix of prefixes) try {
1935
- await execFileAsync("openclaw", [
1936
- "config",
1937
- "unset",
1938
- prefix
1939
- ], { timeout: 1e4 });
1940
- } catch (err) {
1941
- log.warn({
1942
- err: err instanceof Error ? err.message : String(err),
1943
- prefix
1944
- }, "Failed to unset MCP server config via openclaw config unset");
1945
- }
1946
- tracking._mcpServers = Object.fromEntries(Object.entries(mcpTracking).filter(([key]) => key !== integrationId));
1947
- this.writeTracking(tracking);
1948
- }
1949
1867
  isAvailable() {
1950
1868
  return Promise.resolve(existsSync(this.home));
1951
1869
  }
@@ -1963,6 +1881,128 @@ var OpenClawApplier = class {
1963
1881
  }
1964
1882
  };
1965
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
+ * Awaits `manager.flush()` before returning so the openclaw.json
1905
+ * mirror write has fired by the time the caller proceeds. Without
1906
+ * this, an integration activation's success ack would race ahead of
1907
+ * the runtime's openclaw.json update — the cloud would see
1908
+ * `actual.status = active` while OpenClaw still has the stale MCP
1909
+ * set in memory, violating the actual-status protocol contract.
1910
+ */
1911
+ async applyForIntegration(integrationId, servers, mergedConfig) {
1912
+ const owner = `integration:${integrationId}`;
1913
+ const applied = [];
1914
+ for (const server of servers) {
1915
+ const id = `${integrationId}-${server.id}`;
1916
+ const envResolved = await this.resolveEnv(server, mergedConfig);
1917
+ if (envResolved == null) {
1918
+ log.info({
1919
+ integrationId,
1920
+ server: server.id,
1921
+ provider: server.requires_credentials
1922
+ }, "Skipping MCP server registration — required credentials missing or fetch failed");
1923
+ continue;
1924
+ }
1925
+ await this.manager.addServer(toBundlerConfig(server, envResolved), {
1926
+ id,
1927
+ owner
1928
+ });
1929
+ applied.push(id);
1930
+ }
1931
+ await this.manager.flush();
1932
+ return applied;
1933
+ }
1934
+ /**
1935
+ * Drop every entry owned by this integration. Like `applyForIntegration`,
1936
+ * awaits the mirror flush so the runtime is signalled before the call
1937
+ * resolves — the uninstall ack shouldn't outrun openclaw.json being
1938
+ * cleaned up.
1939
+ */
1940
+ async removeForIntegration(integrationId) {
1941
+ const owner = `integration:${integrationId}`;
1942
+ const removed = await this.manager.removeServersByOwner(owner);
1943
+ await this.manager.flush();
1944
+ return removed;
1945
+ }
1946
+ async resolveEnv(server, mergedConfig) {
1947
+ if (!server.env || Object.keys(server.env).length === 0) return {};
1948
+ const needsCredentials = server.requires_credentials;
1949
+ let credentialsCache;
1950
+ if (needsCredentials) {
1951
+ try {
1952
+ credentialsCache = await this.credentials.getCredentials(needsCredentials);
1953
+ } catch (err) {
1954
+ log.warn({
1955
+ err: errMsg(err),
1956
+ provider: needsCredentials
1957
+ }, "Credentials fetch threw — skipping MCP registration");
1958
+ return null;
1959
+ }
1960
+ if (!credentialsCache || Object.keys(credentialsCache).length === 0) return null;
1961
+ }
1962
+ const resolved = {};
1963
+ for (const [key, value] of Object.entries(server.env)) {
1964
+ const interpolated = interpolateString(value, mergedConfig, credentialsCache, this.platform);
1965
+ if (needsCredentials && hasCredentialsPlaceholder(interpolated)) {
1966
+ log.warn({
1967
+ provider: needsCredentials,
1968
+ key
1969
+ }, "Credentials interpolation left a placeholder — skipping MCP registration");
1970
+ return null;
1971
+ }
1972
+ resolved[key] = interpolated;
1973
+ }
1974
+ return resolved;
1975
+ }
1976
+ };
1977
+ function hasCredentialsPlaceholder(value) {
1978
+ return /\{\{credentials\.[a-z0-9-]+\.[a-zA-Z0-9_]+\}\}/.test(value);
1979
+ }
1980
+ function interpolateString(value, mergedConfig, credentials, platform) {
1981
+ let next = value.replace(CONFIG_TEMPLATE_RE, (match, configKey) => {
1982
+ const v = mergedConfig[configKey];
1983
+ return typeof v === "string" || typeof v === "number" ? String(v) : match;
1984
+ });
1985
+ if (credentials) next = next.replace(CREDENTIALS_TEMPLATE_RE, (match, _provider, field) => {
1986
+ const v = credentials[field];
1987
+ return typeof v === "string" || typeof v === "number" ? String(v) : match;
1988
+ });
1989
+ next = next.replace(ALFE_TEMPLATE_RE, (match, platformKey) => {
1990
+ const v = platform[platformKey];
1991
+ return typeof v === "string" || typeof v === "number" ? String(v) : match;
1992
+ });
1993
+ return next;
1994
+ }
1995
+ function toBundlerConfig(server, resolvedEnv) {
1996
+ const cfg = { command: server.command };
1997
+ if (server.args && server.args.length > 0) cfg.args = server.args;
1998
+ if (Object.keys(resolvedEnv).length > 0) cfg.env = resolvedEnv;
1999
+ if (server.cwd) cfg.cwd = server.cwd;
2000
+ return cfg;
2001
+ }
2002
+ function errMsg(err) {
2003
+ return err instanceof Error ? err.message : String(err);
2004
+ }
2005
+ //#endregion
1966
2006
  //#region src/adapter.ts
1967
2007
  var IntegrationManagerAdapter = class {
1968
2008
  constructor(manager) {
@@ -2021,4 +2061,4 @@ var IntegrationManagerAdapter = class {
2021
2061
  }
2022
2062
  };
2023
2063
  //#endregion
2024
- export { Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
2064
+ 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.32",
3
+ "version": "0.1.0",
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.0"
17
18
  },
18
19
  "files": [
19
20
  "dist"