zelari-code 2.20.0 → 2.21.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.
@@ -55304,39 +55304,224 @@ var init_toolRegistry2 = __esm({
55304
55304
  }
55305
55305
  });
55306
55306
 
55307
+ // src/cli/mcp/httpTransport.ts
55308
+ var SSE_DATA_RE, ABORT_GRACE_MS, McpHttpTransport;
55309
+ var init_httpTransport = __esm({
55310
+ "src/cli/mcp/httpTransport.ts"() {
55311
+ "use strict";
55312
+ SSE_DATA_RE = /^data:\s?(.*)$/;
55313
+ ABORT_GRACE_MS = 250;
55314
+ McpHttpTransport = class {
55315
+ constructor(opts) {
55316
+ this.opts = opts;
55317
+ }
55318
+ sessionId = null;
55319
+ closed = false;
55320
+ reinit = null;
55321
+ controllers = /* @__PURE__ */ new Set();
55322
+ get hasSession() {
55323
+ return this.sessionId !== null;
55324
+ }
55325
+ /**
55326
+ * Deliver one JSON-RPC message. Resolves once the response (if any) has
55327
+ * been fed to onMessage. Transport-level failures are converted into
55328
+ * JSON-RPC error responses so the client's pending map rejects cleanly
55329
+ * through the same pump used for stdio.
55330
+ */
55331
+ async send(msg, timeoutMs2) {
55332
+ if (this.closed) throw new Error(`[mcp:${this.opts.serverName}] transport closed`);
55333
+ try {
55334
+ await this.post(msg, timeoutMs2, false);
55335
+ } catch (err) {
55336
+ if (msg.id !== void 0) {
55337
+ this.opts.onMessage({
55338
+ jsonrpc: "2.0",
55339
+ id: msg.id,
55340
+ error: {
55341
+ code: -32e3,
55342
+ message: err instanceof Error ? err.message : String(err)
55343
+ }
55344
+ });
55345
+ }
55346
+ }
55347
+ }
55348
+ /** Best-effort session teardown (HTTP DELETE), then abort in-flight POSTs. */
55349
+ close() {
55350
+ this.closed = true;
55351
+ for (const ac of this.controllers) ac.abort();
55352
+ this.controllers.clear();
55353
+ const sid = this.sessionId;
55354
+ this.sessionId = null;
55355
+ if (!sid) return;
55356
+ const headers2 = {
55357
+ ...this.opts.headers ?? {},
55358
+ "mcp-session-id": sid
55359
+ };
55360
+ void fetch(this.opts.url, { method: "DELETE", headers: headers2 }).catch(() => {
55361
+ });
55362
+ }
55363
+ // ── internals ────────────────────────────────────────────────────────
55364
+ async post(msg, timeoutMs2, replayed) {
55365
+ const ac = new AbortController();
55366
+ this.controllers.add(ac);
55367
+ const timer = setTimeout(() => ac.abort(), timeoutMs2 + ABORT_GRACE_MS);
55368
+ const hadSession = this.sessionId !== null;
55369
+ try {
55370
+ const headers2 = {
55371
+ "content-type": "application/json",
55372
+ accept: "application/json, text/event-stream",
55373
+ ...this.opts.headers ?? {}
55374
+ };
55375
+ if (this.sessionId) headers2["mcp-session-id"] = this.sessionId;
55376
+ const res = await fetch(this.opts.url, {
55377
+ method: "POST",
55378
+ headers: headers2,
55379
+ signal: ac.signal,
55380
+ body: JSON.stringify({ jsonrpc: "2.0", ...msg })
55381
+ });
55382
+ const sid = res.headers.get("mcp-session-id");
55383
+ if (sid) this.sessionId = sid;
55384
+ if (res.status === 404 && hadSession && !replayed && msg.id !== void 0) {
55385
+ this.sessionId = null;
55386
+ await this.ensureSession();
55387
+ return this.post(msg, timeoutMs2, true);
55388
+ }
55389
+ if (!res.ok && res.status !== 202) {
55390
+ throw new Error(`HTTP ${res.status} ${res.statusText}`.trim());
55391
+ }
55392
+ if (msg.id === void 0) {
55393
+ await res.body?.cancel();
55394
+ return;
55395
+ }
55396
+ const contentType = res.headers.get("content-type") ?? "";
55397
+ if (contentType.includes("text/event-stream")) {
55398
+ await this.readSse(res);
55399
+ } else {
55400
+ const body = await res.text();
55401
+ if (body) this.opts.onMessage(JSON.parse(body));
55402
+ }
55403
+ } finally {
55404
+ clearTimeout(timer);
55405
+ this.controllers.delete(ac);
55406
+ }
55407
+ }
55408
+ /** Dedupe concurrent session-loss recoveries into one handshake. */
55409
+ async ensureSession() {
55410
+ if (!this.reinit) {
55411
+ this.reinit = this.opts.onSessionLost().finally(() => {
55412
+ this.reinit = null;
55413
+ }).catch(() => {
55414
+ });
55415
+ }
55416
+ await this.reinit;
55417
+ }
55418
+ /** Minimal SSE reader: dispatch complete `data:` events to onMessage. */
55419
+ async readSse(res) {
55420
+ const reader = res.body?.getReader();
55421
+ if (!reader) return;
55422
+ const decoder = new TextDecoder();
55423
+ let buf = "";
55424
+ let data = "";
55425
+ const dispatch = () => {
55426
+ const payload = data.trim();
55427
+ data = "";
55428
+ if (!payload) return;
55429
+ try {
55430
+ this.opts.onMessage(JSON.parse(payload));
55431
+ } catch {
55432
+ }
55433
+ };
55434
+ for (; ; ) {
55435
+ const { done, value } = await reader.read();
55436
+ if (done) break;
55437
+ buf += decoder.decode(value, { stream: true });
55438
+ let nl;
55439
+ while ((nl = buf.indexOf("\n")) >= 0) {
55440
+ const line = buf.slice(0, nl).replace(/\r$/, "");
55441
+ buf = buf.slice(nl + 1);
55442
+ if (line === "") {
55443
+ dispatch();
55444
+ continue;
55445
+ }
55446
+ const m = SSE_DATA_RE.exec(line);
55447
+ if (m) data += (data ? "\n" : "") + m[1];
55448
+ }
55449
+ }
55450
+ dispatch();
55451
+ }
55452
+ };
55453
+ }
55454
+ });
55455
+
55307
55456
  // src/cli/mcp/mcpClient.ts
55308
55457
  import { spawn as spawn14 } from "node:child_process";
55309
- var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient;
55458
+ var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MAX_LIST_PAGES, MCP_PROTOCOL_VERSION, McpClient;
55310
55459
  var init_mcpClient = __esm({
55311
55460
  "src/cli/mcp/mcpClient.ts"() {
55312
55461
  "use strict";
55313
55462
  init_cmdline();
55314
55463
  init_updater();
55464
+ init_httpTransport();
55315
55465
  DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
55316
55466
  INIT_TIMEOUT_MS = 15e3;
55467
+ MAX_LIST_PAGES = 50;
55317
55468
  MCP_PROTOCOL_VERSION = "2025-03-26";
55318
55469
  McpClient = class {
55319
55470
  constructor(serverName, config2) {
55320
55471
  this.serverName = serverName;
55321
55472
  this.config = config2;
55473
+ this.defaultTimeoutMs = config2.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
55474
+ this.serial = config2.serial ?? this.transportKind() === "http";
55322
55475
  }
55323
55476
  child = null;
55477
+ transport = null;
55324
55478
  nextId = 1;
55325
55479
  pending = /* @__PURE__ */ new Map();
55326
55480
  stdoutBuffer = "";
55327
55481
  closed = false;
55328
- /** Spawn the server process and run the MCP initialize handshake. */
55482
+ /** Tail of the serial queue (config.serial); requests run one at a time. */
55483
+ queueTail = Promise.resolve();
55484
+ /** True while the 404-recovery handshake runs: bypasses the serial queue
55485
+ * (the queued request that triggered the 404 is waiting on this handshake). */
55486
+ recovering = false;
55487
+ serial;
55488
+ defaultTimeoutMs;
55489
+ transportKind() {
55490
+ if (this.config.type) return this.config.type;
55491
+ return this.config.url && !this.config.command ? "http" : "stdio";
55492
+ }
55493
+ /** Connect (spawn / HTTP session) and run the MCP initialize handshake. */
55329
55494
  async start() {
55330
- if (this.child) return;
55495
+ if (this.child || this.transport) return;
55496
+ if (this.transportKind() === "http") {
55497
+ const url2 = this.config.url;
55498
+ if (!url2) {
55499
+ throw new Error(`[mcp:${this.serverName}] http server requires a url`);
55500
+ }
55501
+ this.transport = new McpHttpTransport({
55502
+ serverName: this.serverName,
55503
+ url: url2,
55504
+ // env may carry an explicit Authorization header for remote servers.
55505
+ headers: this.config.env?.AUTHORIZATION ? { Authorization: this.config.env.AUTHORIZATION } : void 0,
55506
+ onMessage: (msg) => this.handleMessage(msg),
55507
+ onSessionLost: () => this.handshake()
55508
+ });
55509
+ await this.handshake();
55510
+ return;
55511
+ }
55512
+ const command = this.config.command;
55513
+ if (!command) {
55514
+ throw new Error(`[mcp:${this.serverName}] stdio server requires a command`);
55515
+ }
55331
55516
  const spawnOpts = {
55332
55517
  stdio: ["pipe", "pipe", "pipe"],
55333
55518
  env: { ...process.env, ...this.config.env ?? {} },
55334
55519
  windowsHide: true
55335
55520
  };
55336
- const child = process.platform === "win32" ? spawn14(buildCmdLine(this.config.command, this.config.args ?? []), {
55521
+ const child = process.platform === "win32" ? spawn14(buildCmdLine(command, this.config.args ?? []), {
55337
55522
  ...spawnOpts,
55338
55523
  shell: true
55339
- }) : spawn14(this.config.command, this.config.args ?? [], spawnOpts);
55524
+ }) : spawn14(command, this.config.args ?? [], spawnOpts);
55340
55525
  this.child = child;
55341
55526
  child.stdout.setEncoding("utf8");
55342
55527
  child.stdout.on("data", (chunk) => this.onStdout(chunk));
@@ -55355,37 +55540,60 @@ var init_mcpClient = __esm({
55355
55540
  );
55356
55541
  }
55357
55542
  });
55358
- await this.request(
55359
- "initialize",
55360
- {
55361
- protocolVersion: MCP_PROTOCOL_VERSION,
55362
- capabilities: {},
55363
- clientInfo: { name: "zelari-code", version: getCurrentVersion() }
55364
- },
55365
- INIT_TIMEOUT_MS
55366
- );
55367
- this.notify("notifications/initialized", {});
55543
+ await this.handshake();
55544
+ }
55545
+ async handshake() {
55546
+ const prev2 = this.recovering;
55547
+ this.recovering = true;
55548
+ try {
55549
+ await this.request(
55550
+ "initialize",
55551
+ {
55552
+ protocolVersion: MCP_PROTOCOL_VERSION,
55553
+ capabilities: {},
55554
+ clientInfo: { name: "zelari-code", version: getCurrentVersion() }
55555
+ },
55556
+ INIT_TIMEOUT_MS
55557
+ );
55558
+ this.notify("notifications/initialized", {});
55559
+ } finally {
55560
+ this.recovering = prev2;
55561
+ }
55368
55562
  }
55369
- /** Discover the server's tools. */
55563
+ /**
55564
+ * Discover the server's tools. Follows `nextCursor` pagination so
55565
+ * large servers (hundreds of tools) are listed completely.
55566
+ */
55370
55567
  async listTools() {
55371
- const res = await this.request("tools/list", {});
55372
- return (res.tools ?? []).filter(
55373
- (t) => !!t.name
55374
- ).map((t) => ({
55375
- name: t.name,
55376
- description: t.description ?? "",
55377
- inputSchema: t.inputSchema ?? { type: "object", properties: {} }
55378
- }));
55568
+ const tools = [];
55569
+ let cursor;
55570
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
55571
+ const res = await this.request(
55572
+ "tools/list",
55573
+ cursor ? { cursor } : {}
55574
+ );
55575
+ for (const t of res.tools ?? []) {
55576
+ if (!t.name) continue;
55577
+ tools.push({
55578
+ name: t.name,
55579
+ description: t.description ?? "",
55580
+ inputSchema: t.inputSchema ?? { type: "object", properties: {} }
55581
+ });
55582
+ }
55583
+ cursor = typeof res.nextCursor === "string" ? res.nextCursor : void 0;
55584
+ if (!cursor) break;
55585
+ }
55586
+ return tools;
55379
55587
  }
55380
55588
  /**
55381
55589
  * Call a tool. Returns the concatenated text content; non-text content
55382
55590
  * items are summarized by type. Throws when the server flags isError.
55383
55591
  */
55384
- async callTool(name, args, timeoutMs2 = DEFAULT_REQUEST_TIMEOUT_MS) {
55592
+ async callTool(name, args, timeoutMs2) {
55385
55593
  const res = await this.request(
55386
55594
  "tools/call",
55387
55595
  { name, arguments: args },
55388
- timeoutMs2
55596
+ timeoutMs2 ?? this.defaultTimeoutMs
55389
55597
  );
55390
55598
  const text = (res.content ?? []).map(
55391
55599
  (c) => c.type === "text" && typeof c.text === "string" ? c.text : `[${c.type ?? "unknown"} content]`
@@ -55394,20 +55602,39 @@ var init_mcpClient = __esm({
55394
55602
  throw new Error(text || `tool "${name}" reported an error`);
55395
55603
  return text;
55396
55604
  }
55397
- /** Terminate the server process and reject all in-flight requests. */
55605
+ /** Terminate the server / session and reject all in-flight requests. */
55398
55606
  close() {
55399
55607
  this.closed = true;
55400
55608
  this.failAll(new Error(`[mcp:${this.serverName}] client closed`));
55609
+ if (this.transport) {
55610
+ this.transport.close();
55611
+ this.transport = null;
55612
+ return;
55613
+ }
55401
55614
  this.child?.kill();
55402
55615
  this.child = null;
55403
55616
  }
55404
- // ── JSON-RPC plumbing ────────────────────────────────────────────────
55405
- request(method, params, timeoutMs2 = DEFAULT_REQUEST_TIMEOUT_MS) {
55617
+ // ── JSON-RPC plumbing (shared by both transports) ────────────────────
55618
+ request(method, params, timeoutMs2) {
55619
+ const effective = timeoutMs2 ?? this.defaultTimeoutMs;
55620
+ if (!this.serial || this.recovering)
55621
+ return this.dispatch(method, params, effective);
55622
+ const run = this.queueTail.then(
55623
+ () => this.dispatch(method, params, effective),
55624
+ () => this.dispatch(method, params, effective)
55625
+ );
55626
+ this.queueTail = run.then(
55627
+ () => void 0,
55628
+ () => void 0
55629
+ );
55630
+ return run;
55631
+ }
55632
+ dispatch(method, params, timeoutMs2) {
55633
+ const transport = this.transport;
55406
55634
  const child = this.child;
55407
- if (!child)
55635
+ if (!transport && !child)
55408
55636
  return Promise.reject(new Error(`[mcp:${this.serverName}] not started`));
55409
55637
  const id3 = this.nextId++;
55410
- const payload = JSON.stringify({ jsonrpc: "2.0", id: id3, method, params });
55411
55638
  return new Promise((resolve9, reject) => {
55412
55639
  const timer = setTimeout(() => {
55413
55640
  this.pending.delete(id3);
@@ -55418,16 +55645,25 @@ var init_mcpClient = __esm({
55418
55645
  );
55419
55646
  }, timeoutMs2);
55420
55647
  this.pending.set(id3, { resolve: resolve9, reject, timer });
55421
- child.stdin.write(payload + "\n", (err) => {
55422
- if (err) {
55423
- clearTimeout(timer);
55424
- this.pending.delete(id3);
55425
- reject(err);
55426
- }
55427
- });
55648
+ if (transport) {
55649
+ void transport.send({ id: id3, method, params }, timeoutMs2);
55650
+ } else {
55651
+ const payload = JSON.stringify({ jsonrpc: "2.0", id: id3, method, params });
55652
+ child.stdin.write(payload + "\n", (err) => {
55653
+ if (err) {
55654
+ clearTimeout(timer);
55655
+ this.pending.delete(id3);
55656
+ reject(err);
55657
+ }
55658
+ });
55659
+ }
55428
55660
  });
55429
55661
  }
55430
55662
  notify(method, params) {
55663
+ if (this.transport) {
55664
+ void this.transport.send({ method, params }, this.defaultTimeoutMs);
55665
+ return;
55666
+ }
55431
55667
  this.child?.stdin.write(
55432
55668
  JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n"
55433
55669
  );
@@ -55439,26 +55675,29 @@ var init_mcpClient = __esm({
55439
55675
  const line = this.stdoutBuffer.slice(0, nl).trim();
55440
55676
  this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1);
55441
55677
  if (!line) continue;
55442
- let msg;
55443
55678
  try {
55444
- msg = JSON.parse(line);
55679
+ this.handleMessage(JSON.parse(line));
55445
55680
  } catch {
55446
55681
  continue;
55447
55682
  }
55448
- if (typeof msg.id !== "number") continue;
55449
- const pending = this.pending.get(msg.id);
55450
- if (!pending) continue;
55451
- this.pending.delete(msg.id);
55452
- clearTimeout(pending.timer);
55453
- if (msg.error) {
55454
- pending.reject(
55455
- new Error(
55456
- `[mcp:${this.serverName}] ${msg.error.message ?? "JSON-RPC error"} (code ${msg.error.code ?? "?"})`
55457
- )
55458
- );
55459
- } else {
55460
- pending.resolve(msg.result);
55461
- }
55683
+ }
55684
+ }
55685
+ /** Route one parsed JSON-RPC message to its pending request (both transports). */
55686
+ handleMessage(msg) {
55687
+ const m = msg;
55688
+ if (typeof m.id !== "number") return;
55689
+ const pending = this.pending.get(m.id);
55690
+ if (!pending) return;
55691
+ this.pending.delete(m.id);
55692
+ clearTimeout(pending.timer);
55693
+ if (m.error) {
55694
+ pending.reject(
55695
+ new Error(
55696
+ `[mcp:${this.serverName}] ${m.error.message ?? "JSON-RPC error"} (code ${m.error.code ?? "?"})`
55697
+ )
55698
+ );
55699
+ } else {
55700
+ pending.resolve(m.result);
55462
55701
  }
55463
55702
  }
55464
55703
  failAll(err) {
@@ -55493,11 +55732,17 @@ function readFile6(path91) {
55493
55732
  const parsed = JSON.parse(readFileSync29(path91, "utf8"));
55494
55733
  const out = {};
55495
55734
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
55496
- if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
55735
+ const hasCommand = !!cfg && typeof cfg.command === "string" && !!cfg.command.trim();
55736
+ const hasUrl = !!cfg && typeof cfg.url === "string" && /^https?:\/\//i.test(cfg.url);
55737
+ if (!cfg || !hasCommand && !hasUrl) continue;
55497
55738
  out[name] = {
55498
- command: cfg.command.trim(),
55739
+ command: hasCommand ? cfg.command.trim() : void 0,
55499
55740
  args: Array.isArray(cfg.args) ? cfg.args.map(String) : void 0,
55500
55741
  env: cfg.env && typeof cfg.env === "object" ? cfg.env : void 0,
55742
+ type: hasUrl && !hasCommand ? "http" : cfg.type === "http" ? "http" : "stdio",
55743
+ url: hasUrl ? cfg.url.trim() : void 0,
55744
+ timeoutMs: typeof cfg.timeoutMs === "number" && cfg.timeoutMs > 0 ? cfg.timeoutMs : void 0,
55745
+ serial: typeof cfg.serial === "boolean" ? cfg.serial : void 0,
55501
55746
  enabled: cfg.enabled !== false
55502
55747
  };
55503
55748
  }
@@ -55539,8 +55784,13 @@ function upsertMcpServer(opts) {
55539
55784
  error: "Invalid server name (use letters, digits, _ -)"
55540
55785
  };
55541
55786
  }
55542
- if (!opts.config.command?.trim()) {
55543
- return { ok: false, error: "command is required" };
55787
+ const hasCommand = !!opts.config.command?.trim();
55788
+ const hasUrl = typeof opts.config.url === "string" && /^https?:\/\//i.test(opts.config.url);
55789
+ if (!hasCommand && !hasUrl) {
55790
+ return {
55791
+ ok: false,
55792
+ error: "either command (stdio) or url (http) is required"
55793
+ };
55544
55794
  }
55545
55795
  let path91;
55546
55796
  if (opts.scope === "user") {
@@ -55557,9 +55807,13 @@ function upsertMcpServer(opts) {
55557
55807
  }
55558
55808
  const current = readFile6(path91);
55559
55809
  current[name] = {
55560
- command: opts.config.command.trim(),
55810
+ command: hasCommand ? opts.config.command.trim() : void 0,
55561
55811
  args: opts.config.args,
55562
55812
  env: opts.config.env,
55813
+ type: hasUrl ? "http" : opts.config.type === "http" ? "http" : "stdio",
55814
+ url: hasUrl ? opts.config.url.trim() : void 0,
55815
+ timeoutMs: opts.config.timeoutMs,
55816
+ serial: opts.config.serial,
55563
55817
  enabled: opts.config.enabled !== false
55564
55818
  };
55565
55819
  writeFile2(path91, current);
@@ -55634,9 +55888,35 @@ function buildQwenMmPreset() {
55634
55888
  ]
55635
55889
  };
55636
55890
  }
55891
+ function buildUnrealPreset() {
55892
+ const url2 = process.env.UNREAL_MCP_URL?.trim() || "http://127.0.0.1:8000/mcp";
55893
+ return {
55894
+ id: "unreal-mcp",
55895
+ servers: {
55896
+ "unreal-mcp": {
55897
+ type: "http",
55898
+ url: url2,
55899
+ // Editor tool calls (asset scans, builds, PIE) can be slow.
55900
+ timeoutMs: 12e4,
55901
+ // Epic guidance: never overlap calls on the editor's game thread.
55902
+ serial: true,
55903
+ enabled: true
55904
+ }
55905
+ },
55906
+ notes: [
55907
+ "Unreal Engine 5.8+ \u2014 MCP server embedded in the editor (Experimental feature).",
55908
+ "Editor: enable the 'Model Context Protocol' plugin, then Edit \u2192 Project Settings \u2192 Plugins \u2192 MCP Server.",
55909
+ `Endpoint: ${url2} (override with UNREAL_MCP_URL; port/path configurable in the editor).`,
55910
+ "Tool Search ON by default: tools surface as list_toolsets / describe_toolset / call_tool.",
55911
+ "Requests are serialized and time out after 120s (editor tools can be slow).",
55912
+ "Start the editor before OR after zelari \u2014 unreachable servers are retried on each turn.",
55913
+ "Kill switch: disable the unreal-mcp server in mcp.json or ZELARI_MCP=0"
55914
+ ]
55915
+ };
55916
+ }
55637
55917
  function listMcpPresetIds() {
55638
55918
  return Object.keys(PRESETS).filter(
55639
- (k) => k === "cua" || k === "composio" || k === "qwen-mm-plugins"
55919
+ (k) => k === "cua" || k === "composio" || k === "qwen-mm-plugins" || k === "unreal-mcp"
55640
55920
  );
55641
55921
  }
55642
55922
  function getMcpPreset(id3) {
@@ -55705,7 +55985,10 @@ var init_mcpPresets = __esm({
55705
55985
  "cua-driver": () => CUA_DRIVER_PRESET,
55706
55986
  composio: buildComposioPreset,
55707
55987
  "qwen-mm-plugins": buildQwenMmPreset,
55708
- "qwen-mm": buildQwenMmPreset
55988
+ "qwen-mm": buildQwenMmPreset,
55989
+ unreal: buildUnrealPreset,
55990
+ "unreal-mcp": buildUnrealPreset,
55991
+ unrealEditor: buildUnrealPreset
55709
55992
  };
55710
55993
  }
55711
55994
  });
@@ -55735,7 +56018,9 @@ function readMcpConfig(projectRoot = process.cwd(), opts) {
55735
56018
  try {
55736
56019
  const parsed = JSON.parse(readFileSync30(p3, "utf8"));
55737
56020
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
55738
- if (!cfg || typeof cfg.command !== "string" || cfg.command.length === 0) continue;
56021
+ const hasCommand = !!cfg && typeof cfg.command === "string" && cfg.command.length > 0;
56022
+ const hasUrl = !!cfg && typeof cfg.url === "string" && /^https?:\/\//i.test(cfg.url);
56023
+ if (!cfg || !hasCommand && !hasUrl) continue;
55739
56024
  merged[name] = cfg;
55740
56025
  }
55741
56026
  } catch {
@@ -55776,16 +56061,52 @@ async function ensureLoaded(projectRoot) {
55776
56061
  }
55777
56062
  } catch (err) {
55778
56063
  client.close();
55779
- state2.warnings.push(`[mcp:${name}] disabled: ${err instanceof Error ? err.message : String(err)}`);
56064
+ const msg = err instanceof Error ? err.message : String(err);
56065
+ if (isHttpConfig(cfg)) {
56066
+ state2.pendingHttp.push({ name, cfg });
56067
+ state2.warnings.push(
56068
+ `[mcp:${name}] not reachable yet (${msg}) \u2014 will retry each turn`
56069
+ );
56070
+ } else {
56071
+ state2.warnings.push(`[mcp:${name}] disabled: ${msg}`);
56072
+ }
55780
56073
  }
55781
56074
  }
55782
56075
  }
56076
+ function isHttpConfig(cfg) {
56077
+ return cfg.type === "http" || !!cfg.url && !cfg.command;
56078
+ }
56079
+ async function retryPendingHttp() {
56080
+ if (state2.pendingHttp.length === 0) return;
56081
+ const remaining = [];
56082
+ for (const p3 of state2.pendingHttp) {
56083
+ const client = new McpClient(p3.name, p3.cfg);
56084
+ try {
56085
+ await client.start();
56086
+ const tools = await client.listTools();
56087
+ state2.clients.push(client);
56088
+ for (const info of tools) {
56089
+ state2.tools.push({
56090
+ registryName: sanitizeToolName(`mcp_${p3.name}_${info.name}`),
56091
+ serverName: p3.name,
56092
+ info,
56093
+ client
56094
+ });
56095
+ }
56096
+ } catch {
56097
+ client.close();
56098
+ remaining.push(p3);
56099
+ }
56100
+ }
56101
+ state2.pendingHttp = remaining;
56102
+ }
55783
56103
  function sanitizeToolName(raw) {
55784
56104
  return raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
55785
56105
  }
55786
56106
  async function registerMcpTools(registry4, projectRoot = process.cwd(), opts) {
55787
56107
  if (process.env["ZELARI_MCP"] === "0") return { registered: [], warnings: [] };
55788
56108
  await ensureLoaded(projectRoot);
56109
+ await retryPendingHttp();
55789
56110
  const skipCuaForCouncil = opts?.councilMode === true && !isCuaAllowedForCouncil();
55790
56111
  const registered = [];
55791
56112
  for (const t of state2.tools) {
@@ -55823,6 +56144,7 @@ function closeMcpClients() {
55823
56144
  state2.tools = [];
55824
56145
  state2.loaded = false;
55825
56146
  state2.warnings = [];
56147
+ state2.pendingHttp = [];
55826
56148
  }
55827
56149
  function _resetMcpForTests() {
55828
56150
  closeMcpClients();
@@ -55836,7 +56158,7 @@ var init_mcpManager = __esm({
55836
56158
  init_mcpClient();
55837
56159
  init_mcpPresets();
55838
56160
  init_folderTrust();
55839
- state2 = { loaded: false, tools: [], warnings: [], clients: [] };
56161
+ state2 = { loaded: false, tools: [], warnings: [], clients: [], pendingHttp: [] };
55840
56162
  }
55841
56163
  });
55842
56164
 
@@ -67537,6 +67859,7 @@ var init_skillConfigIo = __esm({
67537
67859
  "@zelari/core/skills/builtin/planning",
67538
67860
  "@zelari/core/skills/builtin/refactoring",
67539
67861
  "@zelari/core/skills/builtin/review",
67862
+ "@zelari/core/skills/builtin/unrealEditor",
67540
67863
  "@zelari/core/skills/builtin/testing",
67541
67864
  "@zelari/core/skills/builtin/schema-loop",
67542
67865
  "@zelari/core/skills/builtin/computer-use-cua",
@@ -80423,7 +80746,7 @@ function pickRootComponent() {
80423
80746
  }
80424
80747
  if (argv.includes("--help") || argv.includes("-h")) {
80425
80748
  console.log(
80426
- "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --inspect [--json] Unified project inspection (config, skills, MCP,\n hooks, plugins, AGENTS.md, trust status)\n --trust [path] Trust the cwd (or path) so project MCP + hooks load\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --fix-budget Set recommended ZELARI_MAX_TOOL_LOOP_HARD=180,\n ZELARI_MAX_TOOL_LOOP_ITERATIONS=60, ZELARI_CONTEXT_LIMIT=400000\n at User scope (prevents the agent stopping mid-task)\n --permission-mcp <socket> MCP stdio server for external agent permission prompts\n (spawned by claude --permission-prompt-tool)\n --memory-mcp Optional MCP stdio server for project memory\n --cwd <path> Trusted project root (requires ZELARI_MEMORY_MCP=1)\n --client-id <id> Stable local owner identity for private memories\n --memory-json <json> Read-only project-memory bridge for Zelari Desktop\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode kraken|council|zelari Dispatch mode (default: kraken; agent=alias)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n --profile <id> Capability profile (minimal/v1|kraken/v1|council/v1|mission/v1)\n --resume <id> Continue a 2.0 spine session\n --export-session <path> Write zelari-session-export/1 after the run\n --strict-done Evidence-based BUILD completion gate\n --session-export <id> Print a portable 2.0 session export (no LLM)\n serve Companion host for Android/remote clients (Tailscale)\n --bind <ip> Listen address (default: 127.0.0.1; use Tailscale IP)\n --port <n> Port (default: 7421)\n --token <secret> Bearer token (default: ~/.zelari-code/companion.token)\n --project <path> Allowlisted project root (repeatable)\n --save-projects Persist --project list to companion.json\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --thinking <spec> Thinking effort (auto|off|low|medium|high|budget:N)\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --login-oauth Start subscription OAuth (grok, chatgpt, anthropic)\n --provider <id> grok | chatgpt | anthropic\n --code <paste> Anthropic magic-link code (CODE#STATE)\n --no-browser Do not open the system browser\n --refresh-oauth Force-refresh an OAuth access token\n --provider <id> grok | chatgpt | anthropic\n --logout-oauth Clear stored OAuth credentials\n --provider <id> grok | chatgpt | anthropic\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable (required)\n --args <json> JSON array of args (optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-skills Print skills (builtin + user + project)\n --cwd <path> Project root for .zelari/skills\n --set-skill Add/update a SKILL.md skill\n --name <id> Skill id (required)\n --description <t> One-line description (required)\n --body <text> Markdown instructions (required)\n --category <c> plan|refactor|debug|review|test|docs|ops|git|db|maint\n --tools <csv> Comma-separated required tools (optional)\n --cost <l> low|medium|high (default: medium)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-skill Remove a user/project SKILL.md\n --name <id> --scope user|project [--cwd <path>]\n --generate-skill-from-url Fetch URL + draft skill via model (JSON)\n --url <https://...> Required\n --provider <id> Override active provider\n --model <name> Override model for the selected provider\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ZELARI_MEMORY_MCP=1 Enable the optional external memory MCP server\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
80749
+ "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --inspect [--json] Unified project inspection (config, skills, MCP,\n hooks, plugins, AGENTS.md, trust status)\n --trust [path] Trust the cwd (or path) so project MCP + hooks load\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --fix-budget Set recommended ZELARI_MAX_TOOL_LOOP_HARD=180,\n ZELARI_MAX_TOOL_LOOP_ITERATIONS=60, ZELARI_CONTEXT_LIMIT=400000\n at User scope (prevents the agent stopping mid-task)\n --permission-mcp <socket> MCP stdio server for external agent permission prompts\n (spawned by claude --permission-prompt-tool)\n --memory-mcp Optional MCP stdio server for project memory\n --cwd <path> Trusted project root (requires ZELARI_MEMORY_MCP=1)\n --client-id <id> Stable local owner identity for private memories\n --memory-json <json> Read-only project-memory bridge for Zelari Desktop\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode kraken|council|zelari Dispatch mode (default: kraken; agent=alias)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n --profile <id> Capability profile (minimal/v1|kraken/v1|council/v1|mission/v1)\n --resume <id> Continue a 2.0 spine session\n --export-session <path> Write zelari-session-export/1 after the run\n --strict-done Evidence-based BUILD completion gate\n --session-export <id> Print a portable 2.0 session export (no LLM)\n serve Companion host for Android/remote clients (Tailscale)\n --bind <ip> Listen address (default: 127.0.0.1; use Tailscale IP)\n --port <n> Port (default: 7421)\n --token <secret> Bearer token (default: ~/.zelari-code/companion.token)\n --project <path> Allowlisted project root (repeatable)\n --save-projects Persist --project list to companion.json\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --thinking <spec> Thinking effort (auto|off|low|medium|high|budget:N)\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --login-oauth Start subscription OAuth (grok, chatgpt, anthropic)\n --provider <id> grok | chatgpt | anthropic\n --code <paste> Anthropic magic-link code (CODE#STATE)\n --no-browser Do not open the system browser\n --refresh-oauth Force-refresh an OAuth access token\n --provider <id> grok | chatgpt | anthropic\n --logout-oauth Clear stored OAuth credentials\n --provider <id> grok | chatgpt | anthropic\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable for stdio servers\n --url <endpoint> Streamable HTTP endpoint (e.g. UE 5.8 editor)\n --args <json> JSON array of args (stdio, optional)\n --timeout <ms> Per-server request timeout (http, optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --preset unreal-mcp Unreal Engine 5.8+ editor (MCP over HTTP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-skills Print skills (builtin + user + project)\n --cwd <path> Project root for .zelari/skills\n --set-skill Add/update a SKILL.md skill\n --name <id> Skill id (required)\n --description <t> One-line description (required)\n --body <text> Markdown instructions (required)\n --category <c> plan|refactor|debug|review|test|docs|ops|git|db|maint\n --tools <csv> Comma-separated required tools (optional)\n --cost <l> low|medium|high (default: medium)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-skill Remove a user/project SKILL.md\n --name <id> --scope user|project [--cwd <path>]\n --generate-skill-from-url Fetch URL + draft skill via model (JSON)\n --url <https://...> Required\n --provider <id> Override active provider\n --model <name> Override model for the selected provider\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ZELARI_MEMORY_MCP=1 Enable the optional external memory MCP server\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
80427
80750
  );
80428
80751
  process.exit(0);
80429
80752
  }
@@ -80449,6 +80772,8 @@ function pickRootComponent() {
80449
80772
  };
80450
80773
  const name = get("--name");
80451
80774
  const command = get("--command");
80775
+ const url2 = get("--url");
80776
+ const timeoutRaw = get("--timeout");
80452
80777
  const scopeRaw = get("--scope") ?? "user";
80453
80778
  const scope = scopeRaw === "project" ? "project" : "user";
80454
80779
  const cwd = get("--cwd") ?? process.cwd();
@@ -80461,14 +80786,22 @@ function pickRootComponent() {
80461
80786
  if (!Array.isArray(parsed)) throw new Error("--args must be a JSON array");
80462
80787
  args = parsed.map(String);
80463
80788
  }
80464
- if (!name || !command) {
80465
- throw new Error("--name and --command are required");
80789
+ const timeoutMs2 = timeoutRaw !== void 0 ? Number(timeoutRaw) : void 0;
80790
+ if (timeoutMs2 !== void 0 && (!Number.isFinite(timeoutMs2) || timeoutMs2 <= 0)) {
80791
+ throw new Error("--timeout must be a positive number of milliseconds");
80792
+ }
80793
+ if (!name) throw new Error("--name is required");
80794
+ if (!command && !url2) {
80795
+ throw new Error("either --command (stdio) or --url (http) is required");
80796
+ }
80797
+ if (url2 && !/^https?:\/\//i.test(url2)) {
80798
+ throw new Error("--url must be an http(s) endpoint");
80466
80799
  }
80467
80800
  const result = upsertMcpServer({
80468
80801
  scope,
80469
80802
  name,
80470
80803
  projectRoot: cwd,
80471
- config: { command, args, enabled }
80804
+ config: url2 ? { type: "http", url: url2, timeoutMs: timeoutMs2, serial: true, enabled } : { command, args, enabled }
80472
80805
  });
80473
80806
  if (!result.ok) throw new Error(result.error);
80474
80807
  console.log(JSON.stringify({ ok: true, path: result.path, name, scope }));