@alfe.ai/gateway 0.1.0 → 0.1.2

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 (2) hide show
  1. package/dist/health.js +473 -23
  2. package/package.json +7 -7
package/dist/health.js CHANGED
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
6
6
  import { dirname, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import pino from "pino";
9
- import { chmodSync, existsSync, readFileSync, unlinkSync } from "node:fs";
9
+ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import { getEndpointFromToken, readConfig } from "@alfe.ai/config";
11
11
  import crypto from "crypto";
12
12
  import { parse } from "smol-toml";
@@ -14,7 +14,7 @@ import WebSocket from "ws";
14
14
  import { createConnection, createServer } from "node:net";
15
15
  import { IntegrationManager, IntegrationManagerAdapter, McpApplier, OpenClawApplier } from "@alfe.ai/integrations";
16
16
  import { AgentApiClient } from "@alfe.ai/agent-api-client";
17
- import { Manager } from "@alfe.ai/mcp-bundler";
17
+ import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
18
18
  import stream, { Readable } from "stream";
19
19
  import util, { format } from "util";
20
20
  import http from "http";
@@ -93,7 +93,9 @@ const ID_PREFIXES = {
93
93
  role: "role",
94
94
  directGrant: "dgr",
95
95
  oauthConnection: "con",
96
+ channel: "chn",
96
97
  oauthConnectionRequest: "crq",
98
+ deviceCode: "dvc",
97
99
  attachment: "att",
98
100
  run: "run",
99
101
  request: "req",
@@ -259,31 +261,41 @@ var AlfeApiClient = class {
259
261
  this.getToken = options.getToken;
260
262
  this.onAuthFailure = options.onAuthFailure;
261
263
  }
262
- /** Shared fetch logic — handles auth, 401, and network errors. */
263
- async _fetch(path, options) {
264
+ /**
265
+ * Shared fetch logic — handles auth, 401, and network errors.
266
+ *
267
+ * `skipAuth` callers (public endpoints like OAuth device-code) opt out of
268
+ * the auth-header injection AND the synthetic 401 short-circuit. Without
269
+ * this opt-out, a CLI calling /auth/device-code (no token yet by design)
270
+ * would never reach the network — the `getToken: () => null` path would
271
+ * synthesize a 401 and fire onAuthFailure, breaking the entire flow.
272
+ */
273
+ async _fetch(path, options, skipAuth = false) {
264
274
  try {
265
- const token = await this.getToken();
266
- if (!token) {
267
- this.onAuthFailure?.();
268
- return {
269
- ok: false,
270
- result: {
271
- ok: false,
272
- error: "No auth token available",
273
- status: 401
274
- }
275
- };
276
- }
277
275
  const url = `${this.apiBaseUrl}${path}`;
278
276
  const headers = new Headers(options?.headers);
279
- headers.set("Authorization", `Bearer ${token}`);
280
277
  headers.set("Content-Type", "application/json");
281
278
  headers.set("x-correlation-id", correlationId());
279
+ if (!skipAuth) {
280
+ const token = await this.getToken();
281
+ if (!token) {
282
+ this.onAuthFailure?.();
283
+ return {
284
+ ok: false,
285
+ result: {
286
+ ok: false,
287
+ error: "No auth token available",
288
+ status: 401
289
+ }
290
+ };
291
+ }
292
+ headers.set("Authorization", `Bearer ${token}`);
293
+ }
282
294
  const res = await fetch(url, {
283
295
  ...options,
284
296
  headers
285
297
  });
286
- if (res.status === 401) {
298
+ if (res.status === 401 && !skipAuth) {
287
299
  this.onAuthFailure?.();
288
300
  return {
289
301
  ok: false,
@@ -331,6 +343,20 @@ var AlfeApiClient = class {
331
343
  };
332
344
  }
333
345
  /**
346
+ * Make a request to a PUBLIC endpoint that does not require authentication.
347
+ * Skips both the Authorization header injection AND the onAuthFailure
348
+ * callback. Use for endpoints like /auth/device-code that the CLI hits
349
+ * before it has a token.
350
+ */
351
+ async publicRequest(path, options) {
352
+ const result = await this._fetch(path, options, true);
353
+ if (!result.ok) return result.result;
354
+ return {
355
+ ok: true,
356
+ data: result.body.data
357
+ };
358
+ }
359
+ /**
334
360
  * Make an authenticated request that returns the body directly (no envelope unwrap).
335
361
  * Use for APIs that don't use the @auriclabs/api-core response format (e.g. gateway).
336
362
  */
@@ -372,6 +398,55 @@ var AuthService = class {
372
398
  deleteToken(tokenId) {
373
399
  return this.client.request(`${this.prefix}/tokens/${tokenId}`, { method: "DELETE" });
374
400
  }
401
+ /**
402
+ * Start a device-code flow. Called by the CLI when the user runs
403
+ * `alfe login` and picks the browser path. The returned `device_code`
404
+ * is the CLI's bearer secret for polling /auth/device-token; the
405
+ * `user_code` is what the user types/sees on the dashboard.
406
+ *
407
+ * Uses `publicRequest` because the CLI has no token yet — the standard
408
+ * `request` path would short-circuit with a synthetic 401.
409
+ */
410
+ startDeviceCode(input) {
411
+ return this.client.publicRequest(`${this.prefix}/device-code`, {
412
+ method: "POST",
413
+ body: JSON.stringify(input)
414
+ });
415
+ }
416
+ /**
417
+ * Poll for the minted API key. Public endpoint. Returns:
418
+ * 200 → { api_key, tenantId, tokenExpiresAt } (success — write to config)
419
+ * 428 → still pending (caller should sleep `interval` and retry)
420
+ * 429 → polling too fast; caller should back off
421
+ * 410 → device-code expired; user must run `alfe login` again
422
+ * 400 → already redeemed (security: do not retry)
423
+ *
424
+ * Surface the HTTP-status code via the standard ApiResult error shape
425
+ * so callers can branch.
426
+ */
427
+ pollDeviceToken(deviceCode) {
428
+ return this.client.publicRequest(`${this.prefix}/device-token`, {
429
+ method: "POST",
430
+ body: JSON.stringify({ device_code: deviceCode })
431
+ });
432
+ }
433
+ /** Dashboard-side. Fetch the device fingerprint for an approval card. */
434
+ lookupDeviceCode(userCode) {
435
+ return this.client.request(`${this.prefix}/device-code/lookup`, {
436
+ method: "POST",
437
+ body: JSON.stringify({ user_code: userCode })
438
+ });
439
+ }
440
+ /** Dashboard-side. Approve the pending device-code; CLI's next poll gets the key. */
441
+ approveDeviceCode(input) {
442
+ return this.client.request(`${this.prefix}/device-code/approve`, {
443
+ method: "POST",
444
+ body: JSON.stringify({
445
+ user_code: input.userCode,
446
+ expiresIn: input.expiresIn
447
+ })
448
+ });
449
+ }
375
450
  getOnboardingStatus() {
376
451
  return this.client.request(`${this.prefix}/onboarding/status`);
377
452
  }
@@ -510,8 +585,9 @@ var IntegrationsService = class {
510
585
  });
511
586
  return this.client.request(`/integrations/scoped/${encodeURIComponent(integrationId)}?${params}`, { method: "DELETE" });
512
587
  }
513
- listIntegrations(agentId) {
514
- return this.client.request(`/integrations/agents/${agentId}`);
588
+ listIntegrations(agentId, options) {
589
+ const qs = options?.includeInherited ? "?includeInherited=true" : "";
590
+ return this.client.request(`/integrations/agents/${agentId}${qs}`);
515
591
  }
516
592
  installIntegration(agentId, data) {
517
593
  return this.client.request(`/integrations/agents/${agentId}`, {
@@ -4276,7 +4352,10 @@ enumValues({
4276
4352
  Zhipu: "zhipu",
4277
4353
  OpenRouter: "openrouter",
4278
4354
  ElevenLabs: "elevenlabs",
4279
- Twilio: "twilio"
4355
+ Twilio: "twilio",
4356
+ ClaudeMax: "claude-max",
4357
+ OpenAICodexMax: "openai-codex-max",
4358
+ GeminiMax: "gemini-max"
4280
4359
  });
4281
4360
  enumValues({
4282
4361
  Month: "month",
@@ -4334,6 +4413,7 @@ enumValues({
4334
4413
  Project: "project",
4335
4414
  Agent: "agent"
4336
4415
  });
4416
+ ["alfe"].filter((id) => id !== "alfe");
4337
4417
  enumValues({
4338
4418
  Public: "public",
4339
4419
  Hidden: "hidden"
@@ -21700,7 +21780,7 @@ var CommandRegistry = class {
21700
21780
  * the gateway when needed and uses `callerScopes: ["operator.admin"]`.
21701
21781
  * 3. Skip iteration if `pending.json` mtime hasn't changed (cheap fast path)
21702
21782
  */
21703
- const execFileAsync$1 = promisify(execFile);
21783
+ const execFileAsync$2 = promisify(execFile);
21704
21784
  const APPROVE_TIMEOUT_MS = 1e4;
21705
21785
  function resolveStateDir(override) {
21706
21786
  if (override) return override;
@@ -21798,7 +21878,7 @@ function startPairingApprovalPoller(opts) {
21798
21878
  const stateDir = resolveStateDir(opts.stateDir);
21799
21879
  const intervalMs = opts.intervalMs ?? 3e4;
21800
21880
  const exec = opts.exec ?? (async (file, args, { timeout }) => {
21801
- const { stdout, stderr } = await execFileAsync$1(file, args, { timeout });
21881
+ const { stdout, stderr } = await execFileAsync$2(file, args, { timeout });
21802
21882
  return {
21803
21883
  stdout,
21804
21884
  stderr
@@ -21840,6 +21920,128 @@ function startPairingApprovalPoller(opts) {
21840
21920
  };
21841
21921
  }
21842
21922
  //#endregion
21923
+ //#region src/openclaw-mcp-cleanup.ts
21924
+ /**
21925
+ * One-shot startup migration that drops stale `mcp.servers.*` entries
21926
+ * from openclaw.json on agents set up before the single-source-of-truth
21927
+ * refactor. Pre-cutover agents had two writers:
21928
+ *
21929
+ * - The old `post_activate.mjs` hook wrote `mcp.servers.<integrationId>`
21930
+ * directly (legacy style — e.g. QA Tester's `mcp.servers.atlassian`).
21931
+ * - The bundler manager mirror-wrote `mcp.servers.<integrationId>-<serverId>`
21932
+ * for entries it owned (newer style).
21933
+ *
21934
+ * Post-cutover the alfe store owns everything and the openclaw.json
21935
+ * mirror is gone. If the legacy entries linger they cause:
21936
+ * 1. claude-cli / codex-cli to native-spawn from openclaw.json AND the
21937
+ * daemon's bundler to spawn from the store → two mcp-atlassian
21938
+ * children, the second one likely missing credentials.
21939
+ * 2. The openclaw-mcp-bundler plugin's `hasNativeOpenclawMcpServers`
21940
+ * check returns true (mcp.servers non-empty) → plugin no-ops on
21941
+ * every backend → MiniMax-backed agents lose tools entirely.
21942
+ *
21943
+ * Strategy: on first daemon start after the upgrade, drop every key in
21944
+ * openclaw.json#mcp.servers that the alfe store ALSO holds (direct id
21945
+ * match) OR that maps to an alfe-store entry by integration owner
21946
+ * (catches the legacy `mcp.servers.<integrationId>` shape where the
21947
+ * new store uses `<integrationId>-<serverId>`). User-added private MCPs
21948
+ * (e.g. `mcp.servers.my-private-thing` with no matching alfe entry) are
21949
+ * preserved.
21950
+ *
21951
+ * Sentinel-gated at `~/.alfe/.openclaw-mirror-migrated` — runs exactly
21952
+ * once per agent.
21953
+ */
21954
+ const execFileAsync$1 = promisify(execFile);
21955
+ const DEFAULT_SENTINEL_PATH = join(homedir(), ".alfe", ".openclaw-mirror-migrated");
21956
+ const defaultOpenclaw = {
21957
+ async listServers() {
21958
+ try {
21959
+ const { stdout } = await execFileAsync$1("openclaw", [
21960
+ "config",
21961
+ "get",
21962
+ "mcp.servers"
21963
+ ], { timeout: 1e4 });
21964
+ const trimmed = stdout.trim();
21965
+ if (!trimmed) return [];
21966
+ const parsed = JSON.parse(trimmed);
21967
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
21968
+ return Object.keys(parsed);
21969
+ } catch (err) {
21970
+ const message = err instanceof Error ? err.message : String(err);
21971
+ if (/not\s+set|not\s+found|undefined|no such key/i.test(message)) return [];
21972
+ throw err;
21973
+ }
21974
+ },
21975
+ async unsetServer(key) {
21976
+ await execFileAsync$1("openclaw", [
21977
+ "config",
21978
+ "unset",
21979
+ `mcp.servers.${key}`
21980
+ ], { timeout: 1e4 });
21981
+ }
21982
+ };
21983
+ /**
21984
+ * Run the cleanup. Idempotent — second call after the sentinel lands
21985
+ * returns immediately. Errors are caught and logged; the daemon must
21986
+ * not abort startup over a best-effort migration.
21987
+ */
21988
+ async function runOpenclawMcpCleanup(opts) {
21989
+ const sentinelPath = opts.sentinelPath ?? DEFAULT_SENTINEL_PATH;
21990
+ const openclaw = opts.openclaw ?? defaultOpenclaw;
21991
+ if (existsSync(sentinelPath)) {
21992
+ opts.logger.debug({ sentinel: sentinelPath }, "openclaw.json mcp.servers cleanup already ran — skipping");
21993
+ return;
21994
+ }
21995
+ let cleanupErr;
21996
+ try {
21997
+ const existing = await openclaw.listServers();
21998
+ if (existing.length === 0) {
21999
+ opts.logger.debug({}, "openclaw.json#mcp.servers is empty — nothing to clean up");
22000
+ writeSentinel(sentinelPath);
22001
+ return;
22002
+ }
22003
+ const stored = opts.manager.listServers();
22004
+ const storeIds = new Set(stored.map((s) => s.id));
22005
+ const integrationOwners = /* @__PURE__ */ new Set();
22006
+ for (const s of stored) if (s.entry.owner.startsWith("integration:")) integrationOwners.add(s.entry.owner.slice(12));
22007
+ const toUnset = [];
22008
+ for (const key of existing) {
22009
+ if (storeIds.has(key)) {
22010
+ toUnset.push(key);
22011
+ continue;
22012
+ }
22013
+ if (integrationOwners.has(key)) toUnset.push(key);
22014
+ }
22015
+ if (toUnset.length === 0) {
22016
+ opts.logger.info({ existingKeys: existing }, "openclaw.json#mcp.servers has entries but none match alfe-managed integrations — preserving as user customizations");
22017
+ writeSentinel(sentinelPath);
22018
+ return;
22019
+ }
22020
+ for (const key of toUnset) try {
22021
+ await openclaw.unsetServer(key);
22022
+ opts.logger.info({ key }, "Cleaned up stale openclaw.json#mcp.servers entry");
22023
+ } catch (err) {
22024
+ opts.logger.warn({
22025
+ key,
22026
+ err: err instanceof Error ? err.message : String(err)
22027
+ }, "Failed to unset stale mcp.servers entry — continuing");
22028
+ }
22029
+ } catch (err) {
22030
+ cleanupErr = err;
22031
+ opts.logger.warn({ err: err instanceof Error ? err.message : String(err) }, "openclaw.json#mcp.servers cleanup hit an error — leaving sentinel un-touched so next start retries");
22032
+ }
22033
+ if (cleanupErr === void 0) writeSentinel(sentinelPath);
22034
+ }
22035
+ function writeSentinel(path) {
22036
+ try {
22037
+ mkdirSync(dirname(path), { recursive: true });
22038
+ writeFileSync(path, `${(/* @__PURE__ */ new Date()).toISOString()}\n`, {
22039
+ encoding: "utf8",
22040
+ mode: 384
22041
+ });
22042
+ } catch {}
22043
+ }
22044
+ //#endregion
21843
22045
  //#region src/daemon.ts
21844
22046
  /**
21845
22047
  * Alfe Gateway Daemon — main entry point.
@@ -21862,6 +22064,8 @@ let ipcServer = null;
21862
22064
  let commandQueue;
21863
22065
  let startedAt;
21864
22066
  let integrationManager;
22067
+ let mcpBundler = null;
22068
+ let mcpManagerRef = null;
21865
22069
  let aiProxyServer = null;
21866
22070
  let runtimeProcess = null;
21867
22071
  let aiProxyUrl = null;
@@ -22126,6 +22330,32 @@ async function startDaemon() {
22126
22330
  apiUrl: config.apiEndpoint
22127
22331
  });
22128
22332
  const mcpManager = new Manager({ logger: logger$1 });
22333
+ mcpManagerRef = mcpManager;
22334
+ mcpBundler = new McpBundler({
22335
+ logger: logger$1,
22336
+ idleTtlMs: 0
22337
+ }, { connect: defaultConnect });
22338
+ await mcpManager.loadIntoBundler(mcpBundler);
22339
+ const warmBundler = (reason) => {
22340
+ if (!mcpBundler) return;
22341
+ mcpBundler.warmup().catch((err) => {
22342
+ logger$1.warn({
22343
+ err: err instanceof Error ? err.message : String(err),
22344
+ reason
22345
+ }, "MCP bundler warmup failed");
22346
+ });
22347
+ };
22348
+ warmBundler("startup");
22349
+ mcpManager.onChange(() => {
22350
+ warmBundler("store change");
22351
+ });
22352
+ logger$1.info("MCP bundler attached to manager — warming children");
22353
+ await runOpenclawMcpCleanup({
22354
+ manager: mcpManager,
22355
+ logger: logger$1
22356
+ }).catch((err) => {
22357
+ logger$1.warn({ err: err instanceof Error ? err.message : String(err) }, "openclaw.json mcp.servers cleanup threw — startup continues");
22358
+ });
22129
22359
  integrationManager = new IntegrationManager({
22130
22360
  runtimeAppliers,
22131
22361
  mcpApplier: new McpApplier({
@@ -22203,8 +22433,15 @@ async function startDaemon() {
22203
22433
  await runtimeProcess.stop();
22204
22434
  logger$1.debug("Runtime process stopped");
22205
22435
  }
22436
+ if (mcpBundler) {
22437
+ logger$1.debug("Stopping MCP bundler...");
22438
+ await mcpBundler.dispose();
22439
+ mcpBundler = null;
22440
+ logger$1.debug("MCP bundler stopped");
22441
+ }
22206
22442
  logger$1.debug("Stopping MCP bundler manager...");
22207
22443
  await mcpManager.dispose();
22444
+ mcpManagerRef = null;
22208
22445
  logger$1.debug("MCP bundler manager stopped");
22209
22446
  if (ipcServer) {
22210
22447
  logger$1.debug("Stopping IPC server...");
@@ -22523,6 +22760,11 @@ function handlePluginRequest(method, params, pluginId) {
22523
22760
  case "status": return Promise.resolve(handleStatus());
22524
22761
  case "integration.list": return Promise.resolve(handleIntegrationList());
22525
22762
  case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
22763
+ case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
22764
+ case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
22765
+ case "mcp.list_servers": return Promise.resolve(handleMcpListServers());
22766
+ case "mcp.add_server": return handleMcpAddServer(params);
22767
+ case "mcp.remove_server": return handleMcpRemoveServer(params);
22526
22768
  default: return Promise.resolve({
22527
22769
  ok: false,
22528
22770
  error: {
@@ -22565,6 +22807,214 @@ function handleIntegrationList() {
22565
22807
  payload: { integrations: integrationManager.list() }
22566
22808
  };
22567
22809
  }
22810
+ /**
22811
+ * Return the daemon-hosted MCP bundler's current namespaced tool catalog.
22812
+ * Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
22813
+ * cheap (in-memory snapshot, no I/O).
22814
+ */
22815
+ function handleMcpListTools(bundler) {
22816
+ if (!bundler) return {
22817
+ ok: false,
22818
+ error: {
22819
+ code: "MCP_BUNDLER_UNAVAILABLE",
22820
+ message: "MCP bundler not initialized"
22821
+ }
22822
+ };
22823
+ return {
22824
+ ok: true,
22825
+ payload: { tools: bundler.listTools() }
22826
+ };
22827
+ }
22828
+ /**
22829
+ * Route a tool call to the appropriate MCP child via the daemon-hosted
22830
+ * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
22831
+ * the raw JSON object the LLM produced.
22832
+ */
22833
+ /**
22834
+ * List every server entry in the alfe bundler store. Lets agents inspect
22835
+ * what they've already registered before adding a new one.
22836
+ */
22837
+ function handleMcpListServers(manager = mcpManagerRef) {
22838
+ if (!manager) return {
22839
+ ok: false,
22840
+ error: {
22841
+ code: "MCP_MANAGER_UNAVAILABLE",
22842
+ message: "MCP manager not initialized"
22843
+ }
22844
+ };
22845
+ return {
22846
+ ok: true,
22847
+ payload: { servers: manager.listServers() }
22848
+ };
22849
+ }
22850
+ /**
22851
+ * Register a new MCP server in the alfe bundler store on behalf of the
22852
+ * agent. Owned as `'manual'` so the agent can later remove it without
22853
+ * an expectedOwner conflict — matches what `alfe mcp add` does from the
22854
+ * CLI. The daemon's bundler will pick the change up via the manager's
22855
+ * store watcher and spawn the child on the next tool-factory call.
22856
+ */
22857
+ async function handleMcpAddServer(params, manager = mcpManagerRef) {
22858
+ if (!manager) return {
22859
+ ok: false,
22860
+ error: {
22861
+ code: "MCP_MANAGER_UNAVAILABLE",
22862
+ message: "MCP manager not initialized"
22863
+ }
22864
+ };
22865
+ const p = params;
22866
+ if (typeof p.id !== "string" || p.id.length === 0) return {
22867
+ ok: false,
22868
+ error: {
22869
+ code: "INVALID_PARAMS",
22870
+ message: "id is required (string)"
22871
+ }
22872
+ };
22873
+ const config = buildServerConfig(p);
22874
+ if (!config) return {
22875
+ ok: false,
22876
+ error: {
22877
+ code: "INVALID_PARAMS",
22878
+ message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
22879
+ }
22880
+ };
22881
+ try {
22882
+ await manager.addServer(config, {
22883
+ id: p.id,
22884
+ owner: "manual"
22885
+ });
22886
+ return {
22887
+ ok: true,
22888
+ payload: { id: p.id }
22889
+ };
22890
+ } catch (err) {
22891
+ const message = err instanceof Error ? err.message : String(err);
22892
+ logger$1.warn({
22893
+ id: p.id,
22894
+ err: message
22895
+ }, "mcp.add_server failed");
22896
+ return {
22897
+ ok: false,
22898
+ error: {
22899
+ code: "MCP_ADD_FAILED",
22900
+ message
22901
+ }
22902
+ };
22903
+ }
22904
+ }
22905
+ function buildServerConfig(p) {
22906
+ if (typeof p.command === "string" && p.command.length > 0) {
22907
+ const cfg = { command: p.command };
22908
+ if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
22909
+ if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
22910
+ const env = {};
22911
+ for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
22912
+ cfg.env = env;
22913
+ }
22914
+ if (typeof p.cwd === "string") cfg.cwd = p.cwd;
22915
+ return cfg;
22916
+ }
22917
+ if (typeof p.url === "string" && p.url.length > 0) {
22918
+ const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
22919
+ const cfg = {
22920
+ url: p.url,
22921
+ transport
22922
+ };
22923
+ if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
22924
+ const headers = {};
22925
+ for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
22926
+ cfg.headers = headers;
22927
+ }
22928
+ return cfg;
22929
+ }
22930
+ return null;
22931
+ }
22932
+ /**
22933
+ * Drop a server entry the agent previously registered. Restricted to
22934
+ * `manual`-owned entries so the agent can't accidentally clobber
22935
+ * integration-installed or CLI-installed servers (the daemon owns
22936
+ * those; the agent can ask the user to uninstall an integration via
22937
+ * the dashboard).
22938
+ */
22939
+ async function handleMcpRemoveServer(params, manager = mcpManagerRef) {
22940
+ if (!manager) return {
22941
+ ok: false,
22942
+ error: {
22943
+ code: "MCP_MANAGER_UNAVAILABLE",
22944
+ message: "MCP manager not initialized"
22945
+ }
22946
+ };
22947
+ const { id } = params;
22948
+ if (typeof id !== "string" || id.length === 0) return {
22949
+ ok: false,
22950
+ error: {
22951
+ code: "INVALID_PARAMS",
22952
+ message: "id is required (string)"
22953
+ }
22954
+ };
22955
+ try {
22956
+ return {
22957
+ ok: true,
22958
+ payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
22959
+ };
22960
+ } catch (err) {
22961
+ const message = err instanceof Error ? err.message : String(err);
22962
+ if (message.includes("owned by")) return {
22963
+ ok: false,
22964
+ error: {
22965
+ code: "MCP_OWNER_MISMATCH",
22966
+ message
22967
+ }
22968
+ };
22969
+ logger$1.warn({
22970
+ id,
22971
+ err: message
22972
+ }, "mcp.remove_server failed");
22973
+ return {
22974
+ ok: false,
22975
+ error: {
22976
+ code: "MCP_REMOVE_FAILED",
22977
+ message
22978
+ }
22979
+ };
22980
+ }
22981
+ }
22982
+ async function handleMcpCallTool(bundler, params) {
22983
+ if (!bundler) return {
22984
+ ok: false,
22985
+ error: {
22986
+ code: "MCP_BUNDLER_UNAVAILABLE",
22987
+ message: "MCP bundler not initialized"
22988
+ }
22989
+ };
22990
+ const { name, args } = params;
22991
+ if (typeof name !== "string" || name.length === 0) return {
22992
+ ok: false,
22993
+ error: {
22994
+ code: "INVALID_PARAMS",
22995
+ message: "name is required (string)"
22996
+ }
22997
+ };
22998
+ try {
22999
+ return {
23000
+ ok: true,
23001
+ payload: await bundler.callTool(name, args)
23002
+ };
23003
+ } catch (err) {
23004
+ const message = err instanceof Error ? err.message : String(err);
23005
+ logger$1.warn({
23006
+ tool: name,
23007
+ err: message
23008
+ }, "mcp.call_tool failed");
23009
+ return {
23010
+ ok: false,
23011
+ error: {
23012
+ code: "MCP_CALL_FAILED",
23013
+ message
23014
+ }
23015
+ };
23016
+ }
23017
+ }
22568
23018
  function handleIntegrationReport(params, pluginId) {
22569
23019
  const { name, status, detail } = params;
22570
23020
  if (!name || !status) return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,12 +22,12 @@
22
22
  "pino-roll": "^1.2.0",
23
23
  "smol-toml": ">=1.6.1",
24
24
  "ws": "^8.18.0",
25
- "@alfe.ai/agent-api-client": "^0.1.4",
26
- "@alfe.ai/ai-proxy-local": "^0.0.9",
27
- "@alfe.ai/config": "^0.0.8",
28
- "@alfe.ai/integration-manifest": "^0.1.0",
29
- "@alfe.ai/integrations": "^0.1.0",
30
- "@alfe.ai/mcp-bundler": "^0.1.0"
25
+ "@alfe.ai/agent-api-client": "^0.2.0",
26
+ "@alfe.ai/ai-proxy-local": "^0.0.10",
27
+ "@alfe.ai/config": "^0.0.9",
28
+ "@alfe.ai/integration-manifest": "^0.2.0",
29
+ "@alfe.ai/integrations": "^0.1.2",
30
+ "@alfe.ai/mcp-bundler": "^0.2.0"
31
31
  },
32
32
  "license": "UNLICENSED",
33
33
  "scripts": {