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