micro-models-agent 0.63.3 → 1.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 (185) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +7 -4
  41. package/dist/i18n/ru.json +7 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1606 -800
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/driver.js +46 -4
  59. package/dist/modules/certification/cli.js +85 -42
  60. package/dist/modules/certification/loader.js +15 -1
  61. package/dist/modules/certification/manifest.js +126 -15
  62. package/dist/modules/certification/runner.js +4 -26
  63. package/dist/modules/certification/scenarios.js +184 -5
  64. package/dist/modules/certification/syntax-scenarios.js +51 -0
  65. package/dist/modules/context/chunk-query.js +25 -5
  66. package/dist/modules/context/fact-extractor.js +6 -2
  67. package/dist/modules/context/manager.js +23 -7
  68. package/dist/modules/execution/audit-runners.js +7 -1
  69. package/dist/modules/execution/auditor.js +3 -3
  70. package/dist/modules/execution/execution-plugin.js +22 -15
  71. package/dist/modules/execution/input-from.js +46 -0
  72. package/dist/modules/execution/module.js +107 -18
  73. package/dist/modules/execution/moe-executor.js +166 -54
  74. package/dist/modules/execution/plan-actions.js +524 -0
  75. package/dist/modules/execution/plan-steps.js +23 -0
  76. package/dist/modules/execution/plan-store.js +15 -3
  77. package/dist/modules/execution/plan-tool.js +6 -488
  78. package/dist/modules/execution/plan-validator.js +24 -0
  79. package/dist/modules/execution/stuck-detector.js +3 -18
  80. package/dist/modules/execution/tracker.js +14 -5
  81. package/dist/modules/execution/transient-error.js +30 -0
  82. package/dist/modules/execution/verifier.js +94 -7
  83. package/dist/modules/execution/windows-commands.js +11 -0
  84. package/dist/modules/hallucination/confidence.js +36 -23
  85. package/dist/modules/hallucination/consistency.js +3 -0
  86. package/dist/modules/hallucination/detector.js +8 -3
  87. package/dist/modules/hallucination/factual.js +26 -7
  88. package/dist/modules/hallucination/llm-judge.js +12 -2
  89. package/dist/modules/indexer/map-command.js +35 -0
  90. package/dist/modules/indexer/map-select.js +87 -0
  91. package/dist/modules/indexer/module.js +34 -22
  92. package/dist/modules/indexer/symbols.js +189 -0
  93. package/dist/modules/indexer/walker.js +96 -42
  94. package/dist/modules/lsp/check-tool.js +2 -1
  95. package/dist/modules/lsp/client.js +49 -32
  96. package/dist/modules/lsp/config.js +55 -2
  97. package/dist/modules/lsp/module.js +38 -5
  98. package/dist/modules/lsp/probe.js +4 -3
  99. package/dist/modules/lsp/project-root.js +41 -1
  100. package/dist/modules/lsp/startup-check.js +12 -4
  101. package/dist/modules/mcp/client.js +153 -104
  102. package/dist/modules/mcp/module.js +165 -41
  103. package/dist/modules/memory/module.js +4 -3
  104. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  105. package/dist/modules/plugins/manager.js +47 -84
  106. package/dist/modules/pricing/index.js +17 -7
  107. package/dist/modules/pricing/prices.js +30 -12
  108. package/dist/modules/processes/index.js +1 -0
  109. package/dist/modules/processes/kill-tree.js +56 -0
  110. package/dist/modules/processes/registry.js +2 -54
  111. package/dist/modules/providers/cache.js +23 -0
  112. package/dist/modules/providers/factory.js +28 -0
  113. package/dist/modules/providers/fallback.js +7 -5
  114. package/dist/modules/providers/health.js +2 -1
  115. package/dist/modules/providers/index.js +1 -0
  116. package/dist/modules/providers/manager.js +17 -2
  117. package/dist/modules/providers/presets.js +79 -6
  118. package/dist/modules/reasoning/policy.js +40 -0
  119. package/dist/modules/reasoning/probe.js +111 -0
  120. package/dist/modules/security/audit-notifier.js +42 -27
  121. package/dist/modules/security/command-validator.js +25 -20
  122. package/dist/modules/security/encryption.js +6 -12
  123. package/dist/modules/security/network-validator.js +76 -5
  124. package/dist/modules/security/path-validator.js +77 -34
  125. package/dist/modules/security/rate-limiter.js +11 -0
  126. package/dist/modules/security/security-policies.js +1 -1
  127. package/dist/modules/security/session-encryption.js +13 -2
  128. package/dist/modules/security/session-isolation.js +2 -9
  129. package/dist/modules/session/manager.js +11 -0
  130. package/dist/modules/session/module.js +11 -3
  131. package/dist/modules/session/store.js +41 -5
  132. package/dist/modules/skills/loader.js +7 -1
  133. package/dist/modules/skills/module.js +2 -1
  134. package/dist/modules/updater/changelog-reader.js +94 -0
  135. package/dist/modules/updater/dev-detect.js +17 -0
  136. package/dist/modules/updater/index.js +1 -0
  137. package/dist/modules/updater/module.js +14 -3
  138. package/dist/output/bus.js +32 -0
  139. package/dist/output/channel.js +233 -0
  140. package/dist/output/format.js +14 -0
  141. package/dist/output/index.js +7 -0
  142. package/dist/output/json-sink.js +22 -0
  143. package/dist/output/machine.js +8 -0
  144. package/dist/output/session-sink.js +27 -0
  145. package/dist/output/types.js +1 -0
  146. package/dist/tools/approve.js +6 -2
  147. package/dist/tools/attach-image.js +11 -11
  148. package/dist/tools/auto-fixer.js +198 -0
  149. package/dist/tools/bash.js +142 -89
  150. package/dist/tools/chunk-query.js +10 -6
  151. package/dist/tools/download-file.js +1 -1
  152. package/dist/tools/edit-file.js +20 -2
  153. package/dist/tools/executor.js +54 -9
  154. package/dist/tools/glob-tool.js +7 -0
  155. package/dist/tools/grep-tool.js +15 -1
  156. package/dist/tools/index.js +3 -1
  157. package/dist/tools/list-dir.js +3 -1
  158. package/dist/tools/load-skill.js +2 -1
  159. package/dist/tools/mcp-call.js +1 -1
  160. package/dist/tools/move-file.js +5 -4
  161. package/dist/tools/path-utils.js +7 -0
  162. package/dist/tools/pipeline-run.js +1 -1
  163. package/dist/tools/prompt-io.js +28 -0
  164. package/dist/tools/question.js +12 -12
  165. package/dist/tools/scope-request.js +91 -0
  166. package/dist/tools/session-info.js +44 -0
  167. package/dist/tools/set-thinking.js +71 -0
  168. package/dist/tools/subagent.js +50 -9
  169. package/dist/tools/syntax-validator.js +177 -0
  170. package/dist/tools/user-input.js +16 -9
  171. package/dist/tools/write-file.js +17 -1
  172. package/dist/ui/diff.js +10 -0
  173. package/dist/ui/line-editor.js +179 -26
  174. package/dist/ui/line-math.js +20 -3
  175. package/dist/ui/md-formatter.js +100 -10
  176. package/dist/ui/output.js +5 -4
  177. package/dist/ui/plan-view.js +2 -7
  178. package/dist/ui/renderer.js +89 -85
  179. package/dist/ui/spinner.js +14 -4
  180. package/dist/utils/error.js +4 -0
  181. package/dist/utils/index.js +4 -0
  182. package/dist/utils/retry.js +17 -0
  183. package/dist/utils/sleep.js +23 -0
  184. package/dist/utils/truncate.js +9 -0
  185. package/package.json +1 -1
@@ -5,9 +5,12 @@ import { getServerForFile, DEFAULT_LSP_CONFIG } from "./config";
5
5
  import { findProjectRoot } from "./project-root";
6
6
  import { isPathInScope } from "../security/path-validator";
7
7
  import { DEFAULT_SECURITY_CONFIG } from "../../config/security";
8
+ import { toForwardSlash } from "../../tools/path-utils";
8
9
  import { safeResolvePath } from "../../tools/path-utils";
9
10
  import { t } from "../../i18n/index";
10
11
  import { severityLabels, collectCheckFiles, formatCheckDiagnostics } from "./check-tool";
12
+ import { errMsg } from "../../utils";
13
+ import { join } from "path";
11
14
  /**
12
15
  * Consecutive LSP failures before a server is disabled for the session. A dead
13
16
  * server (not installed, npx auto-install failed, path missing) otherwise
@@ -17,6 +20,22 @@ import { severityLabels, collectCheckFiles, formatCheckDiagnostics } from "./che
17
20
  * for that server — a CSS timeout must not take TypeScript checks down too.
18
21
  */
19
22
  const MAX_CONSECUTIVE_LSP_FAILURES = 3;
23
+ /**
24
+ * Module-resolution diagnostics that are meaningless without a type
25
+ * environment. In a bare directory (no tsconfig.json / node_modules) the
26
+ * TypeScript server reports `Cannot find module 'fs'` and
27
+ * `Cannot find name '__dirname'` for perfectly valid Node code — the model
28
+ * then abandons a working approach because of phantom errors (observed
29
+ * ses_mtho2oab: LSP "errors" pushed the model off a planned .ts file to .js
30
+ * and into a plan-drift spiral). Suppressed with an explicit inconclusive
31
+ * note when no type env exists near the project root.
32
+ */
33
+ const TYPE_ENV_DIAG_PATTERN = /Cannot find module ['"][^'"]+['"] or its corresponding type declarations|Cannot find name '(__dirname|__filename|require|process|Buffer|console)'|Do you need to install type definitions/;
34
+ function hasTypeEnvironment(projectRoot) {
35
+ return (existsSync(join(projectRoot, "tsconfig.json")) ||
36
+ existsSync(join(projectRoot, "jsconfig.json")) ||
37
+ existsSync(join(projectRoot, "node_modules")));
38
+ }
20
39
  export class LspModule {
21
40
  name = "lsp";
22
41
  config;
@@ -103,7 +122,7 @@ export class LspModule {
103
122
  // Surface failures instead of silently returning [] — a dead LSP
104
123
  // previously produced ZERO diagnostics across whole sessions and the
105
124
  // model only found type errors reactively via `bun run build`.
106
- const msg = e instanceof Error ? e.message : String(e);
125
+ const msg = errMsg(e);
107
126
  const failures = (self.failuresByServer.get(key) ?? 0) + 1;
108
127
  self.failuresByServer.set(key, failures);
109
128
  try {
@@ -183,6 +202,7 @@ export class LspModule {
183
202
  const notes = [];
184
203
  const checkedFiles = [];
185
204
  let checked = 0;
205
+ let suppressedTypeEnvDiags = 0;
186
206
  for (const file of files) {
187
207
  const serverConfig = getServerForFile(file, this.config);
188
208
  if (!serverConfig)
@@ -192,10 +212,20 @@ export class LspModule {
192
212
  continue;
193
213
  const projectRoot = findProjectRoot(file, ctx.baseDir, serverConfig.workspaceMarkers ?? []);
194
214
  try {
195
- const diags = await this.client.checkFile(file, ctx.baseDir, serverConfig, projectRoot);
215
+ let diags = await this.client.checkFile(file, ctx.baseDir, serverConfig, projectRoot);
196
216
  this.failuresByServer.set(key, 0);
197
217
  checked++;
198
- checkedFiles.push(relative(ctx.baseDir, file).replace(/\\/g, "/"));
218
+ checkedFiles.push(toForwardSlash(relative(ctx.baseDir, file)));
219
+ if (!hasTypeEnvironment(projectRoot)) {
220
+ const kept = diags.filter((d) => {
221
+ if (TYPE_ENV_DIAG_PATTERN.test(d.message)) {
222
+ suppressedTypeEnvDiags++;
223
+ return false;
224
+ }
225
+ return true;
226
+ });
227
+ diags = kept;
228
+ }
199
229
  for (const d of diags) {
200
230
  if (d.severity === 1)
201
231
  errors.push({ file, diag: d });
@@ -204,7 +234,7 @@ export class LspModule {
204
234
  }
205
235
  }
206
236
  catch (e) {
207
- const msg = e instanceof Error ? e.message : String(e);
237
+ const msg = errMsg(e);
208
238
  const failures = (this.failuresByServer.get(key) ?? 0) + 1;
209
239
  this.failuresByServer.set(key, failures);
210
240
  try {
@@ -236,6 +266,9 @@ export class LspModule {
236
266
  if (checked > 0 && errors.length === 0 && warnings.length === 0) {
237
267
  lines.push(t("lsp.check_clean", { count: String(checked) }));
238
268
  }
269
+ if (suppressedTypeEnvDiags > 0) {
270
+ lines.push(`[lsp_check inconclusive: no tsconfig.json / node_modules near project root — ${suppressedTypeEnvDiags} module-resolution diagnostic(s) suppressed. Initialize the project (package.json + types) for accurate TypeScript checks.]`);
271
+ }
239
272
  lines.push(...notes);
240
273
  if (lines.length === 0) {
241
274
  return {
@@ -247,7 +280,7 @@ export class LspModule {
247
280
  }
248
281
  }
249
282
  function formatDiagnostics(diagnostics, filePath) {
250
- const filename = filePath.replace(/\\/g, "/").split("/").pop() ?? filePath;
283
+ const filename = toForwardSlash(filePath).split("/").pop() ?? filePath;
251
284
  return diagnostics
252
285
  .slice(0, 5)
253
286
  .map((d) => {
@@ -1,7 +1,8 @@
1
1
  import { LspClient } from "./client";
2
- import { getLanguageForFile } from "./config";
2
+ import { getServerKeyForFile } from "./config";
3
3
  import { collectCheckFiles } from "./check-tool";
4
4
  import { findProjectRoot } from "./project-root";
5
+ import { errMsg } from "../../utils";
5
6
  const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
6
7
  const DEFAULT_PROBE_DEADLINE_MS = 60_000;
7
8
  const DEFAULT_MAX_PARALLEL = 3;
@@ -21,7 +22,7 @@ export async function probeLspServers(config, baseDir, deps = {}) {
21
22
  const seen = new Set();
22
23
  const servers = [];
23
24
  for (const file of files) {
24
- const language = getLanguageForFile(file);
25
+ const language = getServerKeyForFile(file, config);
25
26
  if (!language)
26
27
  continue;
27
28
  const server = config.servers[language];
@@ -56,7 +57,7 @@ export async function probeLspServers(config, baseDir, deps = {}) {
56
57
  ok.push({ language, ok: true, durationMs: Date.now() - started });
57
58
  }
58
59
  catch (e) {
59
- const errorMsg = e instanceof Error ? e.message : String(e);
60
+ const errorMsg = errMsg(e);
60
61
  deps.logger?.warn(`LSP probe failed for ${language}: ${errorMsg}`, {
61
62
  command: server.command,
62
63
  timeout: serverTimeout,
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "fs";
2
- import { dirname, join } from "path";
2
+ import { dirname, join, relative, isAbsolute } from "path";
3
+ import { platform } from "os";
3
4
  /**
4
5
  * Resolve the LSP workspace root for an edited file.
5
6
  *
@@ -13,11 +14,50 @@ import { dirname, join } from "path";
13
14
  * LSP server config (`LspServerConfig.workspaceMarkers`) — this function takes
14
15
  * the markers as a parameter and performs no language detection itself.
15
16
  */
17
+ /**
18
+ * Находит локальный компилятор TypeScript, поднимаясь от `startDir` вверх по
19
+ * `node_modules`. Возвращает готовую команду или null, если tsc не установлен.
20
+ *
21
+ * Почему не `npx`: обёртка npx тратит 5–13 секунд даже на локальный пакет
22
+ * (Windows), а без локального typescript пытается лезть в сеть. Прямой вызов
23
+ * локального tsc — доли секунды и честная деградация (null → пропускаем).
24
+ *
25
+ * Кросс-платформенность:
26
+ * - предпочитаем JS-энтрипоинт `typescript/bin/tsc` и запускаем его ТЕКУЩИМ
27
+ * рантаймом (`process.execPath` — bun или node). Так не зависим от `node` в
28
+ * PATH (актуально для систем только с Bun) и одинаково работает на
29
+ * Windows/macOS/Linux;
30
+ * - если JS-файла нет, но есть `.bin`-стаб, берём его: на Windows `tsc.cmd`,
31
+ * на macOS/Linux `tsc` (обёртка сама найдёт интерпретатор).
32
+ */
33
+ export function resolveTscCommand(startDir, maxLevels = 8) {
34
+ const runtime = process.execPath;
35
+ let dir = startDir;
36
+ for (let i = 0; i < maxLevels; i++) {
37
+ const js = join(dir, "node_modules", "typescript", "bin", "tsc");
38
+ if (existsSync(js))
39
+ return `"${runtime}" "${js}"`;
40
+ const stub = join(dir, "node_modules", ".bin", platform() === "win32" ? "tsc.cmd" : "tsc");
41
+ if (existsSync(stub))
42
+ return `"${stub}"`;
43
+ const parent = dirname(dir);
44
+ if (parent === dir)
45
+ break;
46
+ dir = parent;
47
+ }
48
+ return null;
49
+ }
16
50
  export function findProjectRoot(filePath, baseDir, markers) {
17
51
  if (!markers || markers.length === 0)
18
52
  return baseDir;
19
53
  let dir = dirname(filePath);
20
54
  const root = baseDir.replace(/[\\/]+$/, "");
55
+ // Clamp the walk to baseDir: callers that pass baseDir itself as filePath
56
+ // (probes) start at dirname(baseDir) — ABOVE the sandbox — and would pick up
57
+ // a marker directory from the parent (e.g. a package.json next to it).
58
+ const rel = relative(root, dir);
59
+ if ((rel && rel.startsWith("..")) || isAbsolute(rel))
60
+ dir = root;
21
61
  while (true) {
22
62
  if (markers.some((m) => existsSync(join(dir, m)))) {
23
63
  return dir;
@@ -5,7 +5,9 @@ import { t } from "../../i18n/index";
5
5
  import { LspClient } from "./client";
6
6
  import { collectCheckFiles } from "./check-tool";
7
7
  import { getServerForFile } from "./config";
8
- import { findProjectRoot } from "./project-root";
8
+ import { findProjectRoot, resolveTscCommand } from "./project-root";
9
+ import { estimateTokens } from "../../llm/token-counter";
10
+ import { errMsg } from "../../utils";
9
11
  /** Startup check never examines more than this many files via LSP. */
10
12
  const STARTUP_CHECK_FILE_CAP = 5;
11
13
  /** Overall cap for the whole startup check — never block session start longer. */
@@ -42,7 +44,7 @@ export async function runStartupHealthCheck(config, baseDir, deps = {}) {
42
44
  content,
43
45
  priority: "high",
44
46
  essential: false,
45
- estimatedTokens: Math.ceil(content.length / 4),
47
+ estimatedTokens: estimateTokens(content),
46
48
  };
47
49
  }
48
50
  catch {
@@ -80,7 +82,7 @@ async function runCheck(config, baseDir, deps) {
80
82
  }
81
83
  catch (e) {
82
84
  // Per-file LSP failure → log and skip (fail-open).
83
- deps.logger?.debug(`Startup LSP check failed for ${file}: ${e instanceof Error ? e.message : String(e)}`);
85
+ deps.logger?.debug(`Startup LSP check failed for ${file}: ${errMsg(e)}`);
84
86
  }
85
87
  }
86
88
  if (checked === 0 || lines.length === 0)
@@ -88,8 +90,14 @@ async function runCheck(config, baseDir, deps) {
88
90
  return { lines: lines.slice(0, STARTUP_CHECK_ERROR_CAP) };
89
91
  }
90
92
  async function runTscDefault(projectRoot, timeoutMs) {
93
+ // Локальный tsc, а не `npx`: npx-обёртка (cmd → node npm-prefix) медленная и
94
+ // на Windows переживает выход процесса, оставляя открытым stdout-пайп —
95
+ // команда «виснет». Нет локального tsc — молча пропускаем проверку.
96
+ const tsc = resolveTscCommand(projectRoot);
97
+ if (!tsc)
98
+ return [];
91
99
  return new Promise((resolve) => {
92
- const child = spawn("npx", ["tsc", "--noEmit", "--skipLibCheck"], {
100
+ const child = spawn(`${tsc} --noEmit --skipLibCheck`, {
93
101
  cwd: projectRoot,
94
102
  shell: true,
95
103
  windowsHide: true,
@@ -1,4 +1,12 @@
1
1
  import { spawn } from "child_process";
2
+ import { defaultOutputBus } from "../../output";
3
+ /** Default timeout for MCP HTTP requests (connect + tool calls). */
4
+ const HTTP_TIMEOUT_MS = 10_000;
5
+ /**
6
+ * Accept header required by the MCP Streamable HTTP transport — the server
7
+ * responds with 406 if the client doesn't accept both media types.
8
+ */
9
+ const MCP_ACCEPT_HEADER = "application/json, text/event-stream";
2
10
  export class MCPClient {
3
11
  serverName;
4
12
  config;
@@ -31,22 +39,30 @@ export class MCPClient {
31
39
  if (!url)
32
40
  throw new Error("SSE transport requires a url");
33
41
  this.abortController = new AbortController();
34
- const response = await fetch(url, {
35
- method: "GET",
36
- headers: {
37
- Accept: "text/event-stream",
38
- ...this.config.headers,
39
- },
40
- signal: this.abortController.signal,
41
- });
42
- if (!response.ok) {
43
- throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
42
+ const timeout = setTimeout(() => this.abortController?.abort(), this.config.timeout || HTTP_TIMEOUT_MS);
43
+ try {
44
+ const response = await fetch(url, {
45
+ method: "GET",
46
+ headers: {
47
+ Accept: "text/event-stream",
48
+ ...this.config.headers,
49
+ },
50
+ signal: this.abortController.signal,
51
+ });
52
+ clearTimeout(timeout);
53
+ if (!response.ok) {
54
+ throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
55
+ }
56
+ this._connected = true;
57
+ const reader = response.body.getReader();
58
+ const decoder = new TextDecoder();
59
+ let buffer = "";
60
+ this.readSSE(reader, decoder, buffer);
61
+ }
62
+ catch (err) {
63
+ clearTimeout(timeout);
64
+ throw err;
44
65
  }
45
- this._connected = true;
46
- const reader = response.body.getReader();
47
- const decoder = new TextDecoder();
48
- let buffer = "";
49
- this.readSSE(reader, decoder, buffer);
50
66
  }
51
67
  async readSSE(reader, decoder, buffer) {
52
68
  try {
@@ -87,6 +103,7 @@ export class MCPClient {
87
103
  try {
88
104
  const msg = JSON.parse(data);
89
105
  if (this.pendingResolve && msg.id) {
106
+ this.clearSseTimer();
90
107
  if (msg.error) {
91
108
  this.pendingReject?.(new Error(msg.error.message));
92
109
  }
@@ -97,10 +114,15 @@ export class MCPClient {
97
114
  this.pendingReject = null;
98
115
  }
99
116
  }
100
- catch { }
117
+ catch (e) {
118
+ // Heartbeats/comments are not JSON — expected noise, but still logged
119
+ // so a genuinely broken stream is visible in diagnostics.
120
+ defaultOutputBus.log("error", "mcp", `[MCPClient] Unparseable SSE message: ${String(data).slice(0, 120)} ${e instanceof Error ? e.message : String(e)}`, { persist: true });
121
+ }
101
122
  }
102
123
  handleSSEError(err) {
103
124
  if (this.pendingReject) {
125
+ this.clearSseTimer();
104
126
  this.pendingReject(err);
105
127
  this.pendingResolve = null;
106
128
  this.pendingReject = null;
@@ -130,11 +152,102 @@ export class MCPClient {
130
152
  child.on("spawn", () => {
131
153
  this._connected = true;
132
154
  this.process = child;
133
- _resolved = true;
134
- resolve();
155
+ // MCP handshake: real servers require `initialize` before any
156
+ // tools/list or tools/call request.
157
+ this.stdioRequest("initialize", {
158
+ protocolVersion: "2024-11-05",
159
+ capabilities: {},
160
+ clientInfo: { name: "mma", version: "1.0.0" },
161
+ }, 10000)
162
+ .then(() => {
163
+ child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
164
+ })
165
+ .then(() => {
166
+ _resolved = true;
167
+ resolve();
168
+ })
169
+ .catch((err) => {
170
+ this._connected = false;
171
+ if (!_resolved) {
172
+ _resolved = true;
173
+ reject(err);
174
+ }
175
+ });
135
176
  });
136
177
  });
137
178
  }
179
+ /** Unparsed trailing bytes from stdio responses (partial line). */
180
+ stdioBuffer = "";
181
+ /**
182
+ * JSON-RPC request over stdio with timeout, exit handling and
183
+ * buffer-aware line parsing (notifications and stale responses between
184
+ * our request and its response no longer break matching).
185
+ */
186
+ stdioRequest(method, params, timeoutMs) {
187
+ const proc = this.process;
188
+ if (!proc || !proc.stdout || !proc.stdin) {
189
+ return Promise.reject(new Error("Not connected to MCP server"));
190
+ }
191
+ const stdout = proc.stdout;
192
+ const timeout = timeoutMs ?? this.config.timeout ?? 30000;
193
+ return new Promise((resolve, reject) => {
194
+ const request = {
195
+ jsonrpc: "2.0",
196
+ id: Date.now(),
197
+ method,
198
+ ...(params !== undefined ? { params } : {}),
199
+ };
200
+ let timer;
201
+ let settled = false;
202
+ const finish = (fn) => {
203
+ if (settled)
204
+ return;
205
+ settled = true;
206
+ if (timer !== undefined)
207
+ clearTimeout(timer);
208
+ stdout.off("data", onData);
209
+ proc.off("exit", onExit);
210
+ fn();
211
+ };
212
+ const onData = (data) => {
213
+ this.stdioBuffer += data.toString();
214
+ let idx;
215
+ while ((idx = this.stdioBuffer.indexOf("\n")) !== -1 && !settled) {
216
+ const line = this.stdioBuffer.slice(0, idx).trim();
217
+ this.stdioBuffer = this.stdioBuffer.slice(idx + 1);
218
+ if (!line)
219
+ continue;
220
+ let msg;
221
+ try {
222
+ msg = JSON.parse(line);
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ if (msg.id !== request.id)
228
+ continue; // notification or foreign response
229
+ finish(() => {
230
+ if (msg.error) {
231
+ reject(new Error(msg.error.message));
232
+ }
233
+ else {
234
+ resolve(msg.result);
235
+ }
236
+ });
237
+ return;
238
+ }
239
+ };
240
+ const onExit = () => {
241
+ finish(() => reject(new Error(`MCP server exited before responding to ${method}`)));
242
+ };
243
+ stdout.on("data", onData);
244
+ proc.once("exit", onExit);
245
+ timer = setTimeout(() => {
246
+ finish(() => reject(new Error(`MCP ${method} timeout`)));
247
+ }, timeout);
248
+ proc.stdin.write(JSON.stringify(request) + "\n");
249
+ });
250
+ }
138
251
  async listTools() {
139
252
  if (!this._connected) {
140
253
  await this.connect();
@@ -156,10 +269,12 @@ export class MCPClient {
156
269
  const response = await fetch(this.config.url, {
157
270
  method: "POST",
158
271
  headers: {
272
+ Accept: MCP_ACCEPT_HEADER,
159
273
  "Content-Type": "application/json",
160
274
  ...this.config.headers,
161
275
  },
162
276
  body: JSON.stringify(request),
277
+ signal: AbortSignal.timeout(this.config.timeout || HTTP_TIMEOUT_MS),
163
278
  });
164
279
  if (!response.ok) {
165
280
  const text = await response.text().catch(() => "");
@@ -180,6 +295,7 @@ export class MCPClient {
180
295
  const response = await fetch(this.config.url, {
181
296
  method: "POST",
182
297
  headers: {
298
+ Accept: MCP_ACCEPT_HEADER,
183
299
  "Content-Type": "application/json",
184
300
  ...this.config.headers,
185
301
  },
@@ -200,46 +316,8 @@ export class MCPClient {
200
316
  if (!this.process) {
201
317
  throw new Error("Not connected to MCP server");
202
318
  }
203
- return new Promise((resolve, reject) => {
204
- const request = {
205
- jsonrpc: "2.0",
206
- id: Date.now(),
207
- method: "tools/list",
208
- };
209
- const requestStr = JSON.stringify(request) + "\n";
210
- this.process.stdin.write(requestStr);
211
- let responseData = "";
212
- const onData = (data) => {
213
- responseData += data.toString();
214
- if (responseData.includes("\n")) {
215
- this.process.stdout.off("data", onData);
216
- try {
217
- const lines = responseData.split("\n").filter((l) => l.trim());
218
- for (const line of lines) {
219
- const response = JSON.parse(line);
220
- if (response.id === request.id) {
221
- if (response.error) {
222
- reject(new Error(response.error.message));
223
- }
224
- else {
225
- resolve(response.result?.tools || []);
226
- }
227
- return;
228
- }
229
- }
230
- reject(new Error("No matching response found"));
231
- }
232
- catch (e) {
233
- reject(new Error(`Parse error: ${e.message}`));
234
- }
235
- }
236
- };
237
- this.process.stdout.on("data", onData);
238
- setTimeout(() => {
239
- this.process.stdout.off("data", onData);
240
- reject(new Error("MCP listTools timeout"));
241
- }, 30000);
242
- });
319
+ const result = await this.stdioRequest("tools/list");
320
+ return result?.tools || [];
243
321
  }
244
322
  async disconnect() {
245
323
  if (this.config.transport === "http") {
@@ -281,10 +359,12 @@ export class MCPClient {
281
359
  const response = await fetch(this.config.url, {
282
360
  method: "POST",
283
361
  headers: {
362
+ Accept: MCP_ACCEPT_HEADER,
284
363
  "Content-Type": "application/json",
285
364
  ...this.config.headers,
286
365
  },
287
366
  body: JSON.stringify(request),
367
+ signal: AbortSignal.timeout(this.config.timeout || HTTP_TIMEOUT_MS),
288
368
  });
289
369
  if (!response.ok) {
290
370
  const text = await response.text().catch(() => "");
@@ -296,6 +376,8 @@ export class MCPClient {
296
376
  }
297
377
  return result.result || result;
298
378
  }
379
+ /** Timer of the in-flight SSE request — cleared when the reply arrives via the stream. */
380
+ sseTimer;
299
381
  async callToolSSE(toolName, args) {
300
382
  const request = {
301
383
  jsonrpc: "2.0",
@@ -310,7 +392,7 @@ export class MCPClient {
310
392
  this.pendingResolve = resolve;
311
393
  this.pendingReject = reject;
312
394
  const timeout = this.config.timeout || 30000;
313
- const timer = setTimeout(() => {
395
+ this.sseTimer = setTimeout(() => {
314
396
  this.pendingResolve = null;
315
397
  this.pendingReject = null;
316
398
  reject(new Error("MCP call timeout"));
@@ -318,6 +400,7 @@ export class MCPClient {
318
400
  fetch(this.config.url, {
319
401
  method: "POST",
320
402
  headers: {
403
+ Accept: MCP_ACCEPT_HEADER,
321
404
  "Content-Type": "application/json",
322
405
  ...this.config.headers,
323
406
  },
@@ -325,7 +408,7 @@ export class MCPClient {
325
408
  signal: this.abortController?.signal,
326
409
  })
327
410
  .then(async (response) => {
328
- clearTimeout(timer);
411
+ this.clearSseTimer();
329
412
  if (!response.ok) {
330
413
  const text = await response.text().catch(() => "");
331
414
  reject(new Error(`MCP POST failed: ${response.status} ${text}`));
@@ -340,60 +423,26 @@ export class MCPClient {
340
423
  }
341
424
  })
342
425
  .catch((err) => {
343
- clearTimeout(timer);
426
+ this.clearSseTimer();
344
427
  if (err.name !== "AbortError") {
345
428
  reject(err);
346
429
  }
347
430
  });
348
431
  });
349
432
  }
433
+ clearSseTimer() {
434
+ if (this.sseTimer !== undefined) {
435
+ clearTimeout(this.sseTimer);
436
+ this.sseTimer = undefined;
437
+ }
438
+ }
350
439
  callToolStdio(toolName, args) {
351
440
  if (!this.process) {
352
441
  throw new Error("Not connected to MCP server");
353
442
  }
354
- return new Promise((resolve, reject) => {
355
- const request = {
356
- jsonrpc: "2.0",
357
- id: Date.now(),
358
- method: "tools/call",
359
- params: {
360
- name: toolName,
361
- arguments: args,
362
- },
363
- };
364
- const requestStr = JSON.stringify(request) + "\n";
365
- this.process.stdin.write(requestStr);
366
- let responseData = "";
367
- const onData = (data) => {
368
- responseData += data.toString();
369
- if (responseData.includes("\n")) {
370
- this.process.stdout.off("data", onData);
371
- try {
372
- const lines = responseData.split("\n").filter((l) => l.trim());
373
- for (const line of lines) {
374
- const response = JSON.parse(line);
375
- if (response.id === request.id) {
376
- if (response.error) {
377
- reject(new Error(response.error.message));
378
- }
379
- else {
380
- resolve(response.result);
381
- }
382
- return;
383
- }
384
- }
385
- reject(new Error("No matching response found"));
386
- }
387
- catch (e) {
388
- reject(new Error(`Parse error: ${e.message}`));
389
- }
390
- }
391
- };
392
- this.process.stdout.on("data", onData);
393
- setTimeout(() => {
394
- this.process.stdout.off("data", onData);
395
- reject(new Error("MCP call timeout"));
396
- }, 30000);
443
+ return this.stdioRequest("tools/call", {
444
+ name: toolName,
445
+ arguments: args,
397
446
  });
398
447
  }
399
448
  }