@copilotkit/aimock 1.44.0 → 1.45.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 (278) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +42 -0
  4. package/README.md +6 -0
  5. package/dist/agui-types.d.cts.map +1 -1
  6. package/dist/aimock-cli.cjs +31 -5
  7. package/dist/aimock-cli.cjs.map +1 -1
  8. package/dist/aimock-cli.d.cts.map +1 -1
  9. package/dist/aimock-cli.d.ts.map +1 -1
  10. package/dist/aimock-cli.js +31 -5
  11. package/dist/aimock-cli.js.map +1 -1
  12. package/dist/bedrock-converse.cjs +312 -39
  13. package/dist/bedrock-converse.cjs.map +1 -1
  14. package/dist/bedrock-converse.d.cts.map +1 -1
  15. package/dist/bedrock-converse.d.ts.map +1 -1
  16. package/dist/bedrock-converse.js +313 -40
  17. package/dist/bedrock-converse.js.map +1 -1
  18. package/dist/bedrock.cjs +195 -42
  19. package/dist/bedrock.cjs.map +1 -1
  20. package/dist/bedrock.d.cts.map +1 -1
  21. package/dist/bedrock.d.ts.map +1 -1
  22. package/dist/bedrock.js +196 -43
  23. package/dist/bedrock.js.map +1 -1
  24. package/dist/chaos.cjs +1 -0
  25. package/dist/chaos.js +1 -1
  26. package/dist/cli.cjs +152 -5
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +155 -8
  29. package/dist/cli.js.map +1 -1
  30. package/dist/cohere.cjs +132 -71
  31. package/dist/cohere.cjs.map +1 -1
  32. package/dist/cohere.d.cts.map +1 -1
  33. package/dist/cohere.d.ts.map +1 -1
  34. package/dist/cohere.js +133 -72
  35. package/dist/cohere.js.map +1 -1
  36. package/dist/config-loader.cjs +62 -4
  37. package/dist/config-loader.cjs.map +1 -1
  38. package/dist/config-loader.d.cts +37 -4
  39. package/dist/config-loader.d.cts.map +1 -1
  40. package/dist/config-loader.d.ts +37 -4
  41. package/dist/config-loader.d.ts.map +1 -1
  42. package/dist/config-loader.js +62 -4
  43. package/dist/config-loader.js.map +1 -1
  44. package/dist/fakes-plugin.cjs +131 -0
  45. package/dist/fakes-plugin.cjs.map +1 -0
  46. package/dist/fakes-plugin.d.cts +23 -0
  47. package/dist/fakes-plugin.d.cts.map +1 -0
  48. package/dist/fakes-plugin.d.ts +23 -0
  49. package/dist/fakes-plugin.d.ts.map +1 -0
  50. package/dist/fakes-plugin.js +127 -0
  51. package/dist/fakes-plugin.js.map +1 -0
  52. package/dist/fixture-loader-services.cjs +1 -1
  53. package/dist/fixture-loader-services.cjs.map +1 -1
  54. package/dist/fixture-loader-services.js +1 -1
  55. package/dist/fixture-loader-services.js.map +1 -1
  56. package/dist/fixture-loader.cjs +389 -27
  57. package/dist/fixture-loader.cjs.map +1 -1
  58. package/dist/fixture-loader.d.cts +47 -3
  59. package/dist/fixture-loader.d.cts.map +1 -1
  60. package/dist/fixture-loader.d.ts +47 -3
  61. package/dist/fixture-loader.d.ts.map +1 -1
  62. package/dist/fixture-loader.js +384 -29
  63. package/dist/fixture-loader.js.map +1 -1
  64. package/dist/gemini-interactions.cjs +250 -52
  65. package/dist/gemini-interactions.cjs.map +1 -1
  66. package/dist/gemini-interactions.d.cts.map +1 -1
  67. package/dist/gemini-interactions.d.ts.map +1 -1
  68. package/dist/gemini-interactions.js +251 -53
  69. package/dist/gemini-interactions.js.map +1 -1
  70. package/dist/gemini.cjs +226 -37
  71. package/dist/gemini.cjs.map +1 -1
  72. package/dist/gemini.d.cts.map +1 -1
  73. package/dist/gemini.d.ts.map +1 -1
  74. package/dist/gemini.js +227 -38
  75. package/dist/gemini.js.map +1 -1
  76. package/dist/helpers.cjs +881 -7
  77. package/dist/helpers.cjs.map +1 -1
  78. package/dist/helpers.d.cts +85 -4
  79. package/dist/helpers.d.cts.map +1 -1
  80. package/dist/helpers.d.ts +85 -4
  81. package/dist/helpers.d.ts.map +1 -1
  82. package/dist/helpers.js +839 -8
  83. package/dist/helpers.js.map +1 -1
  84. package/dist/index.cjs +9 -0
  85. package/dist/index.d.cts +7 -5
  86. package/dist/index.d.ts +7 -5
  87. package/dist/index.js +4 -3
  88. package/dist/jest.cjs +31 -7
  89. package/dist/jest.cjs.map +1 -1
  90. package/dist/jest.d.cts +43 -3
  91. package/dist/jest.d.cts.map +1 -1
  92. package/dist/jest.d.ts +43 -3
  93. package/dist/jest.d.ts.map +1 -1
  94. package/dist/jest.js +33 -9
  95. package/dist/jest.js.map +1 -1
  96. package/dist/journal.cjs +135 -13
  97. package/dist/journal.cjs.map +1 -1
  98. package/dist/journal.d.cts +38 -4
  99. package/dist/journal.d.cts.map +1 -1
  100. package/dist/journal.d.ts +38 -4
  101. package/dist/journal.d.ts.map +1 -1
  102. package/dist/journal.js +135 -13
  103. package/dist/journal.js.map +1 -1
  104. package/dist/live-sanitize.cjs +4 -32
  105. package/dist/live-sanitize.cjs.map +1 -1
  106. package/dist/live-sanitize.js +4 -32
  107. package/dist/live-sanitize.js.map +1 -1
  108. package/dist/llmock.cjs +123 -30
  109. package/dist/llmock.cjs.map +1 -1
  110. package/dist/llmock.d.cts +16 -1
  111. package/dist/llmock.d.cts.map +1 -1
  112. package/dist/llmock.d.ts +16 -1
  113. package/dist/llmock.d.ts.map +1 -1
  114. package/dist/llmock.js +125 -32
  115. package/dist/llmock.js.map +1 -1
  116. package/dist/mcp-fakes-mount.cjs +1 -0
  117. package/dist/mcp-fakes-mount.cjs.map +1 -1
  118. package/dist/mcp-fakes-mount.js +1 -0
  119. package/dist/mcp-fakes-mount.js.map +1 -1
  120. package/dist/mcp-fakes-report.cjs +111 -0
  121. package/dist/mcp-fakes-report.cjs.map +1 -0
  122. package/dist/mcp-fakes-report.d.cts +40 -0
  123. package/dist/mcp-fakes-report.d.cts.map +1 -0
  124. package/dist/mcp-fakes-report.d.ts +40 -0
  125. package/dist/mcp-fakes-report.d.ts.map +1 -0
  126. package/dist/mcp-fakes-report.js +107 -0
  127. package/dist/mcp-fakes-report.js.map +1 -0
  128. package/dist/mcp-fakes.cjs +331 -8
  129. package/dist/mcp-fakes.cjs.map +1 -1
  130. package/dist/mcp-fakes.d.cts +406 -3
  131. package/dist/mcp-fakes.d.cts.map +1 -1
  132. package/dist/mcp-fakes.d.ts +406 -3
  133. package/dist/mcp-fakes.d.ts.map +1 -1
  134. package/dist/mcp-fakes.js +331 -8
  135. package/dist/mcp-fakes.js.map +1 -1
  136. package/dist/mcp-handler.cjs +216 -17
  137. package/dist/mcp-handler.cjs.map +1 -1
  138. package/dist/mcp-handler.d.cts +49 -0
  139. package/dist/mcp-handler.d.cts.map +1 -0
  140. package/dist/mcp-handler.d.ts +49 -0
  141. package/dist/mcp-handler.d.ts.map +1 -0
  142. package/dist/mcp-handler.js +214 -18
  143. package/dist/mcp-handler.js.map +1 -1
  144. package/dist/mcp-mock.cjs +112 -1
  145. package/dist/mcp-mock.cjs.map +1 -1
  146. package/dist/mcp-mock.d.cts +33 -2
  147. package/dist/mcp-mock.d.cts.map +1 -1
  148. package/dist/mcp-mock.d.ts +33 -2
  149. package/dist/mcp-mock.d.ts.map +1 -1
  150. package/dist/mcp-mock.js +112 -1
  151. package/dist/mcp-mock.js.map +1 -1
  152. package/dist/mcp-recorder.cjs +1224 -0
  153. package/dist/mcp-recorder.cjs.map +1 -0
  154. package/dist/mcp-recorder.d.cts +108 -0
  155. package/dist/mcp-recorder.d.cts.map +1 -0
  156. package/dist/mcp-recorder.d.ts +108 -0
  157. package/dist/mcp-recorder.d.ts.map +1 -0
  158. package/dist/mcp-recorder.js +1215 -0
  159. package/dist/mcp-recorder.js.map +1 -0
  160. package/dist/messages.cjs +362 -56
  161. package/dist/messages.cjs.map +1 -1
  162. package/dist/messages.d.cts.map +1 -1
  163. package/dist/messages.d.ts.map +1 -1
  164. package/dist/messages.js +363 -58
  165. package/dist/messages.js.map +1 -1
  166. package/dist/misbehavior.cjs +1523 -0
  167. package/dist/misbehavior.cjs.map +1 -0
  168. package/dist/misbehavior.d.cts +43 -0
  169. package/dist/misbehavior.d.cts.map +1 -0
  170. package/dist/misbehavior.d.ts +43 -0
  171. package/dist/misbehavior.d.ts.map +1 -0
  172. package/dist/misbehavior.js +1511 -0
  173. package/dist/misbehavior.js.map +1 -0
  174. package/dist/ollama.cjs +310 -112
  175. package/dist/ollama.cjs.map +1 -1
  176. package/dist/ollama.d.cts.map +1 -1
  177. package/dist/ollama.d.ts.map +1 -1
  178. package/dist/ollama.js +311 -113
  179. package/dist/ollama.js.map +1 -1
  180. package/dist/openrouter-video.cjs +30 -2
  181. package/dist/openrouter-video.cjs.map +1 -1
  182. package/dist/openrouter-video.d.cts.map +1 -1
  183. package/dist/openrouter-video.d.ts.map +1 -1
  184. package/dist/openrouter-video.js +30 -2
  185. package/dist/openrouter-video.js.map +1 -1
  186. package/dist/record-sanitize.cjs +231 -0
  187. package/dist/record-sanitize.cjs.map +1 -0
  188. package/dist/record-sanitize.js +224 -0
  189. package/dist/record-sanitize.js.map +1 -0
  190. package/dist/recorder.cjs +106 -5
  191. package/dist/recorder.cjs.map +1 -1
  192. package/dist/recorder.d.cts +2 -1
  193. package/dist/recorder.d.cts.map +1 -1
  194. package/dist/recorder.d.ts +2 -1
  195. package/dist/recorder.d.ts.map +1 -1
  196. package/dist/recorder.js +107 -7
  197. package/dist/recorder.js.map +1 -1
  198. package/dist/responses.cjs +667 -171
  199. package/dist/responses.cjs.map +1 -1
  200. package/dist/responses.d.cts +5 -13
  201. package/dist/responses.d.cts.map +1 -1
  202. package/dist/responses.d.ts +5 -13
  203. package/dist/responses.d.ts.map +1 -1
  204. package/dist/responses.js +665 -174
  205. package/dist/responses.js.map +1 -1
  206. package/dist/router.cjs +24 -2
  207. package/dist/router.cjs.map +1 -1
  208. package/dist/router.d.cts.map +1 -1
  209. package/dist/router.d.ts.map +1 -1
  210. package/dist/router.js +25 -4
  211. package/dist/router.js.map +1 -1
  212. package/dist/server.cjs +572 -76
  213. package/dist/server.cjs.map +1 -1
  214. package/dist/server.d.cts.map +1 -1
  215. package/dist/server.d.ts.map +1 -1
  216. package/dist/server.js +574 -78
  217. package/dist/server.js.map +1 -1
  218. package/dist/stream-collapse.cjs +236 -34
  219. package/dist/stream-collapse.cjs.map +1 -1
  220. package/dist/stream-collapse.d.cts +39 -4
  221. package/dist/stream-collapse.d.cts.map +1 -1
  222. package/dist/stream-collapse.d.ts +39 -4
  223. package/dist/stream-collapse.d.ts.map +1 -1
  224. package/dist/stream-collapse.js +235 -35
  225. package/dist/stream-collapse.js.map +1 -1
  226. package/dist/types.cjs.map +1 -1
  227. package/dist/types.d.cts +315 -4
  228. package/dist/types.d.cts.map +1 -1
  229. package/dist/types.d.ts +315 -4
  230. package/dist/types.d.ts.map +1 -1
  231. package/dist/types.js.map +1 -1
  232. package/dist/validate-cli.cjs +36 -4
  233. package/dist/validate-cli.cjs.map +1 -1
  234. package/dist/validate-cli.js +37 -5
  235. package/dist/validate-cli.js.map +1 -1
  236. package/dist/vector-types.d.ts.map +1 -1
  237. package/dist/version.cjs +45 -0
  238. package/dist/version.cjs.map +1 -0
  239. package/dist/version.js +44 -0
  240. package/dist/version.js.map +1 -0
  241. package/dist/vitest.cjs +66 -10
  242. package/dist/vitest.cjs.map +1 -1
  243. package/dist/vitest.d.cts +35 -4
  244. package/dist/vitest.d.cts.map +1 -1
  245. package/dist/vitest.d.ts +35 -4
  246. package/dist/vitest.d.ts.map +1 -1
  247. package/dist/vitest.js +67 -11
  248. package/dist/vitest.js.map +1 -1
  249. package/dist/watcher.cjs +6 -2
  250. package/dist/watcher.cjs.map +1 -1
  251. package/dist/watcher.js +6 -2
  252. package/dist/watcher.js.map +1 -1
  253. package/dist/ws-gemini-live.cjs +210 -101
  254. package/dist/ws-gemini-live.cjs.map +1 -1
  255. package/dist/ws-gemini-live.d.cts +9 -3
  256. package/dist/ws-gemini-live.d.cts.map +1 -1
  257. package/dist/ws-gemini-live.d.ts +9 -3
  258. package/dist/ws-gemini-live.d.ts.map +1 -1
  259. package/dist/ws-gemini-live.js +211 -102
  260. package/dist/ws-gemini-live.js.map +1 -1
  261. package/dist/ws-realtime.cjs +175 -62
  262. package/dist/ws-realtime.cjs.map +1 -1
  263. package/dist/ws-realtime.d.cts +8 -3
  264. package/dist/ws-realtime.d.cts.map +1 -1
  265. package/dist/ws-realtime.d.ts +8 -3
  266. package/dist/ws-realtime.d.ts.map +1 -1
  267. package/dist/ws-realtime.js +176 -63
  268. package/dist/ws-realtime.js.map +1 -1
  269. package/dist/ws-responses.cjs +109 -16
  270. package/dist/ws-responses.cjs.map +1 -1
  271. package/dist/ws-responses.d.cts +8 -12
  272. package/dist/ws-responses.d.cts.map +1 -1
  273. package/dist/ws-responses.d.ts +8 -12
  274. package/dist/ws-responses.d.ts.map +1 -1
  275. package/dist/ws-responses.js +111 -18
  276. package/dist/ws-responses.js.map +1 -1
  277. package/package.json +10 -2
  278. package/skills/write-fixtures/SKILL.md +179 -21
@@ -0,0 +1,1224 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_message_text = require('./message-text.cjs');
3
+ const require_api_key_auth = require('./api-key-auth.cjs');
4
+ const require_helpers = require('./helpers.cjs');
5
+ const require_recorder = require('./recorder.cjs');
6
+ const require_logger = require('./logger.cjs');
7
+ const require_record_sanitize = require('./record-sanitize.cjs');
8
+ const require_types = require('./types.cjs');
9
+ const require_mcp_handler = require('./mcp-handler.cjs');
10
+ const require_mcp_fakes = require('./mcp-fakes.cjs');
11
+ const require_version = require('./version.cjs');
12
+ let node_http = require("node:http");
13
+ node_http = require_runtime.__toESM(node_http);
14
+ let node_crypto = require("node:crypto");
15
+ node_crypto = require_runtime.__toESM(node_crypto);
16
+ let node_https = require("node:https");
17
+ node_https = require_runtime.__toESM(node_https);
18
+ let node_fs = require("node:fs");
19
+ node_fs = require_runtime.__toESM(node_fs);
20
+ let node_path = require("node:path");
21
+ node_path = require_runtime.__toESM(node_path);
22
+ let node_string_decoder = require("node:string_decoder");
23
+
24
+ //#region src/mcp-recorder.ts
25
+ /**
26
+ * MCP record mode (rr: MR1-MR17, par: AM1-AM9): a pass-through proxy inside
27
+ * `MCPMock` that forwards to a real upstream MCP server, relays its answers,
28
+ * and writes the `tools/list` and `tools/call` exchanges it sees as an
29
+ * `mcpFakes` file (FA1-FA5), sanitized (S1-S7).
30
+ *
31
+ * Every file this module writes goes through `sanitizeRecording` and then
32
+ * `persistServiceFakes`; there is no other filesystem write here.
33
+ */
34
+ /** AM4: the protocol versions this recorder writes (legacy era only). */
35
+ const RECORDABLE_VERSIONS = new Set([
36
+ "2025-03-26",
37
+ "2025-06-18",
38
+ "2025-11-25"
39
+ ]);
40
+ /** MR2 (d): default bytes of one recordable response that are buffered. */
41
+ const DEFAULT_RECORD_BUFFER_BYTES = 64 * 1024 * 1024;
42
+ /** MR2 (d): the ceiling `maxRecordBufferBytes` is clamped to. */
43
+ const MAX_RECORD_BUFFER_BYTES = 256 * 1024 * 1024;
44
+ /** The MR14 body of a 502 answered for an upstream auth failure. */
45
+ const AUTH_FAILURE_ERROR = "aimock MCP recording got an upstream auth failure; OAuth discovery is not supported through the recorder; record with a pre-acquired token via upstreamAuth or the client's Authorization header";
46
+ /** Hop-by-hop response headers that are never relayed. */
47
+ const HOP_BY_HOP = new Set([
48
+ "connection",
49
+ "keep-alive",
50
+ "transfer-encoding",
51
+ "te",
52
+ "trailer",
53
+ "upgrade",
54
+ "proxy-authenticate",
55
+ "proxy-authorization"
56
+ ]);
57
+ /** The identity query parameters aimock reads and never forwards (MR2 c). */
58
+ const IDENTITY_QUERY = new Set([
59
+ "testId",
60
+ "context",
61
+ "undeclared"
62
+ ]);
63
+ /** MR7: `_meta` keys dropped from a recorded result. */
64
+ const DROPPED_META = [
65
+ "traceparent",
66
+ "tracestate",
67
+ "baggage",
68
+ "io.modelcontextprotocol/serverInfo"
69
+ ];
70
+ const SERVER_INFO_META = "io.modelcontextprotocol/serverInfo";
71
+ const RELATED_TASK_META = "io.modelcontextprotocol/related-task";
72
+ const PROTOCOL_VERSION_META = "io.modelcontextprotocol/protocolVersion";
73
+ /** The version assumed when nothing names one (MR4 source 4). */
74
+ const FALLBACK_VERSION = "2025-03-26";
75
+ /** A header token (RFC 9110 `token`). */
76
+ const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
77
+ /**
78
+ * MR14 / AM6: parse a `Name: value` header, from `upstreamAuth` or
79
+ * `AIMOCK_MCP_UPSTREAM_AUTH`. `undefined` or an empty string is no header. A
80
+ * malformed value is a start error that names `source`, never the value.
81
+ */
82
+ function parseUpstreamAuth(raw, source = "AIMOCK_MCP_UPSTREAM_AUTH") {
83
+ if (raw === void 0 || raw === "") return void 0;
84
+ const colon = raw.indexOf(":");
85
+ const name = colon === -1 ? "" : raw.slice(0, colon).trim();
86
+ const value = colon === -1 ? "" : raw.slice(colon + 1).trim();
87
+ if (!HEADER_NAME.test(name) || value === "") throw new Error(`${source} must be "Name: value" (a header name, a colon, then a value)`);
88
+ return {
89
+ name,
90
+ value
91
+ };
92
+ }
93
+ /**
94
+ * The MCP recorder of one `MCPMock`. `handle` always answers the request:
95
+ * by a fake (MR4), a fake error (MR5), or by forwarding it (MR2).
96
+ */
97
+ var McpRecorder = class {
98
+ target;
99
+ auth;
100
+ maxBuffer;
101
+ fixturePath;
102
+ secretValues;
103
+ fallbackLogger = new require_logger.Logger("warn");
104
+ /** MR3: upstream `Mcp-Session-Id` → what its `initialize` bound. */
105
+ sessions = /* @__PURE__ */ new Map();
106
+ /** MR16: pending lists per `${mount}\0${scope}`. */
107
+ pendingLists = /* @__PURE__ */ new Map();
108
+ /** MR16: completed lists waiting for their block's first call entry. */
109
+ completedLists = /* @__PURE__ */ new Map();
110
+ /** MR13: resolved path → sha256 of the last write. */
111
+ hashes = /* @__PURE__ */ new Map();
112
+ listeners = /* @__PURE__ */ new Set();
113
+ /** RL3: mounts already warned about a request with no scope. */
114
+ warnedNoScope = /* @__PURE__ */ new Set();
115
+ constructor(host, config) {
116
+ this.host = host;
117
+ this.config = config;
118
+ let target;
119
+ try {
120
+ target = new URL(config.upstream);
121
+ } catch {
122
+ throw new Error("MCP record upstream is not a valid URL");
123
+ }
124
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error("MCP record upstream must be an http: or https: URL");
125
+ this.target = target;
126
+ this.secretValues = config.secretValues ?? [];
127
+ require_record_sanitize.validateSecretValues(this.secretValues);
128
+ this.auth = parseUpstreamAuth(config.upstreamAuth, "upstreamAuth");
129
+ const max = config.maxRecordBufferBytes;
130
+ this.maxBuffer = typeof max === "number" && Number.isFinite(max) && max > 0 ? Math.min(Math.floor(max), MAX_RECORD_BUFFER_BYTES) : DEFAULT_RECORD_BUFFER_BYTES;
131
+ this.fixturePath = config.fixturePath ?? "./fixtures/recorded";
132
+ if (!config.proxyOnly) checkMcpRecordDestination(this.fixturePath);
133
+ }
134
+ /** AM1 entry point. Reads the body itself. Always handles the request. */
135
+ async handle(req, res, mount, subPath) {
136
+ const x = {
137
+ req,
138
+ res,
139
+ mount,
140
+ subPath,
141
+ arrivedAt: performance.now(),
142
+ identity: null,
143
+ supplied: {
144
+ testId: false,
145
+ context: false,
146
+ undeclared: false
147
+ },
148
+ message: null,
149
+ rpcMethod: null,
150
+ rpcId: void 0,
151
+ params: void 0,
152
+ proxied: false
153
+ };
154
+ try {
155
+ const body = await require_helpers.readBody(req);
156
+ const sessionId = headerOf(req, "mcp-session-id");
157
+ const resolution = require_helpers.resolveMcpIdentity(req, sessionId ? this.sessions.get(sessionId) : void 0);
158
+ if (!resolution.ok) {
159
+ res.writeHead(resolution.error.status, { "Content-Type": "application/json" });
160
+ res.end(JSON.stringify({
161
+ error: resolution.error.message,
162
+ code: resolution.error.code
163
+ }));
164
+ return;
165
+ }
166
+ x.identity = resolution.identity;
167
+ x.supplied = resolution.supplied;
168
+ let parsed;
169
+ let parsedOk = false;
170
+ if (body !== "") try {
171
+ parsed = JSON.parse(body);
172
+ parsedOk = true;
173
+ } catch {}
174
+ if (parsedOk && Array.isArray(parsed)) {
175
+ this.skip(x, "batch", "batch");
176
+ await this.forward(x, body, null);
177
+ return;
178
+ }
179
+ if (parsedOk && isObject(parsed) && typeof parsed.method === "string") {
180
+ x.rpcMethod = parsed.method;
181
+ const isRequest = "id" in parsed;
182
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") x.rpcId = parsed.id;
183
+ if (isObject(parsed.params)) x.params = parsed.params;
184
+ if (parsed.method === "tools/call") x.message = parsed;
185
+ if (parsed.method === "initialize" && isRequest) {
186
+ await this.forward(x, body, "initialize");
187
+ return;
188
+ }
189
+ if (parsed.method === "tools/list" && isRequest) {
190
+ await this.forward(x, body, "tools/list");
191
+ return;
192
+ }
193
+ if (parsed.method === "tools/call" && isRequest) {
194
+ if (await this.answerByFake(x, parsed)) return;
195
+ this.logForwardedCall(x);
196
+ await this.forward(x, body, "tools/call");
197
+ return;
198
+ }
199
+ if (isRequest) this.skip(x, "not-tools", parsed.method);
200
+ await this.forward(x, body, null);
201
+ return;
202
+ }
203
+ if (body !== "" && req.method === "POST") this.skip(x, "not-tools", "(no method)");
204
+ await this.forward(x, body, null);
205
+ } catch (err) {
206
+ const thrown = err instanceof Error ? err.message : String(err);
207
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: request error on mount ${require_message_text.plainText(mount)}: ${require_message_text.plainText(thrown)}`));
208
+ if (!res.headersSent) {
209
+ res.writeHead(500);
210
+ res.end("Internal server error");
211
+ } else if (!res.writableEnded) res.end();
212
+ } finally {
213
+ this.journal(x);
214
+ }
215
+ }
216
+ /** MR3 / MR16 reset doors (fakes:R1-R5, R8): one test id, or everything. */
217
+ reset(testId) {
218
+ for (const [id, session] of this.sessions) if (testId === void 0 || session.testId === testId) this.sessions.delete(id);
219
+ for (const lists of [this.pendingLists, this.completedLists]) for (const [key, list] of lists) {
220
+ if (testId !== void 0 && list.testId !== testId) continue;
221
+ lists.delete(key);
222
+ this.log().warn(require_message_text.build(require_message_text.msg`MCP-RECORD: forwarded, not recorded (list-incomplete): tools/list on mount ${require_message_text.plainText(mountOfKey(key))}`));
223
+ }
224
+ }
225
+ /** MR13: the sha256 of the last write to `filepath`, if this recorder wrote it. */
226
+ lastWrittenHash(filepath) {
227
+ return this.hashes.get(node_path.resolve(filepath));
228
+ }
229
+ /** MR13: called after each write with the resolved path and the `mcpFakes` written. */
230
+ onWrite(listener) {
231
+ this.listeners.add(listener);
232
+ return () => void this.listeners.delete(listener);
233
+ }
234
+ /** @internal MR3: whether the record-session map holds `sessionId` (tests). */
235
+ hasRecordSession(sessionId) {
236
+ return this.sessions.has(sessionId);
237
+ }
238
+ /** MR16: drop pending lists (RL2 list-incomplete) and clear the session map. */
239
+ close() {
240
+ this.reset();
241
+ this.listeners.clear();
242
+ }
243
+ /**
244
+ * MR4 / MR5 for one `tools/call` request: answer it by a fake, or by a fake
245
+ * error (deny, strict, eviction), and return true; false to forward it.
246
+ */
247
+ async answerByFake(x, message) {
248
+ const params = message.params;
249
+ const name = isObject(params) ? params.name : void 0;
250
+ const id = message.id;
251
+ if (typeof name !== "string" || name === "" || typeof id !== "string" && typeof id !== "number") return false;
252
+ const identity = x.identity;
253
+ const args = isObject(params) ? params.arguments : void 0;
254
+ const event = {
255
+ identity,
256
+ tool: name,
257
+ mount: x.mount,
258
+ args
259
+ };
260
+ let claim;
261
+ try {
262
+ claim = this.host.fakes.claim(name, args, identity);
263
+ } catch {
264
+ return false;
265
+ }
266
+ if (claim.kind === "answer") {
267
+ x.mcpFake = {
268
+ id: claim.entry.id,
269
+ outcome: "answered"
270
+ };
271
+ this.host.onFakeEvent({
272
+ ...event,
273
+ kind: "answered",
274
+ entryId: claim.entry.id
275
+ });
276
+ await require_mcp_handler.writeFakeToolAnswer(x.req, x.res, id, claim.result, this.host.fakes.replayOf(claim.entry.id), params, this.host.replaySpeed(), null, x.arrivedAt);
277
+ return true;
278
+ }
279
+ const strict = require_helpers.resolveStrictMode(this.config.strict, x.req.headers);
280
+ const ctx = {
281
+ identity,
282
+ mount: x.mount,
283
+ notifications: [],
284
+ calls: []
285
+ };
286
+ let failure = null;
287
+ if (claim.kind === "none") {
288
+ if (this.host.fakes.policy(identity) === "deny" || strict) failure = require_mcp_handler.fakeFailure({ kind: "not_declared" }, name, ctx, () => this.host.fakes.declaredTools(identity));
289
+ } else if (claim.kind === "evicted" || strict) failure = require_mcp_handler.fakeFailure(claim, name, ctx, () => []);
290
+ if (!failure) return false;
291
+ x.mcpFake = {
292
+ id: null,
293
+ outcome: failure.outcome
294
+ };
295
+ this.host.onFakeEvent(failure.outcome === "evicted" ? {
296
+ ...event,
297
+ kind: "evicted",
298
+ code: failure.code,
299
+ message: failure.message,
300
+ ...failure.evicted
301
+ } : {
302
+ ...event,
303
+ kind: failure.outcome,
304
+ code: failure.code,
305
+ message: failure.message
306
+ });
307
+ x.res.writeHead(200, { "Content-Type": "application/json" });
308
+ x.res.end(JSON.stringify({
309
+ jsonrpc: "2.0",
310
+ id,
311
+ error: {
312
+ code: require_types.MCP_FAKE_ERROR_CODES[failure.code],
313
+ message: failure.message,
314
+ data: { aimock: failure.aimock }
315
+ }
316
+ }));
317
+ return true;
318
+ }
319
+ /** RP2: a forwarded `tools/call` is logged as unfaked, answered by the upstream. */
320
+ logForwardedCall(x) {
321
+ const name = x.params?.name;
322
+ if (typeof name !== "string" || x.identity === null) return;
323
+ this.host.fakes.logEvent(x.identity, {
324
+ outcome: "unfaked",
325
+ answeredBy: "upstream",
326
+ tool: name,
327
+ args: x.params?.arguments,
328
+ context: x.identity.context,
329
+ mount: x.mount
330
+ });
331
+ }
332
+ /** Forward one request to the upstream and relay its answer; record it when `kind` says so. */
333
+ forward(x, body, kind) {
334
+ const { req, res } = x;
335
+ x.proxied = true;
336
+ if (require_api_key_auth.isAuthenticatedRequest(req) && !this.auth) {
337
+ res.writeHead(502, { "Content-Type": "application/json" });
338
+ res.end(JSON.stringify({ error: "No configured upstream credential" }));
339
+ return Promise.resolve();
340
+ }
341
+ const headers = require_recorder.buildForwardHeaders(req);
342
+ require_recorder.removeForwardHeader(headers, "x-aimock-mcp-undeclared");
343
+ if (this.auth) {
344
+ require_recorder.removeForwardHeader(headers, this.auth.name);
345
+ headers[this.auth.name] = this.auth.value;
346
+ }
347
+ if (body !== "") headers["content-length"] = String(Buffer.byteLength(body));
348
+ const url = this.upstreamUrl(req, x.subPath);
349
+ const transport = url.protocol === "https:" ? node_https : node_http;
350
+ return new Promise((resolve) => {
351
+ let done = false;
352
+ const finish = () => {
353
+ if (done) return;
354
+ done = true;
355
+ resolve();
356
+ };
357
+ const upstreamReq = transport.request(url, {
358
+ method: req.method ?? "POST",
359
+ headers
360
+ });
361
+ res.on("close", () => {
362
+ if (!upstreamReq.destroyed) upstreamReq.destroy();
363
+ finish();
364
+ });
365
+ upstreamReq.on("error", (err) => {
366
+ if (done) return;
367
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: upstream request failed for ${require_message_text.plainText(x.rpcMethod ?? req.method ?? "?")} on mount ${require_message_text.plainText(x.mount)}: ${require_message_text.plainText(err.message)}`));
368
+ if (!res.headersSent) {
369
+ res.writeHead(502, { "Content-Type": "application/json" });
370
+ res.end(JSON.stringify({ error: "aimock MCP recorder could not reach the upstream" }));
371
+ } else if (!res.writableEnded) res.end();
372
+ finish();
373
+ });
374
+ upstreamReq.on("response", (up) => {
375
+ this.relay(x, up, kind, finish);
376
+ });
377
+ upstreamReq.end(body === "" ? void 0 : body);
378
+ });
379
+ }
380
+ /** The upstream URL of a request: the endpoint plus the sub-path, the query minus identity params. */
381
+ upstreamUrl(req, subPath) {
382
+ const url = new URL(this.target.href);
383
+ if (subPath !== "" && subPath !== "/") url.pathname = url.pathname.replace(/\/+$/, "") + subPath;
384
+ const raw = (req.url ?? "").split("#")[0];
385
+ const q = raw.indexOf("?");
386
+ const kept = q === -1 ? [] : raw.slice(q + 1).split("&").filter((pair) => pair !== "" && !IDENTITY_QUERY.has(queryName(pair)));
387
+ if (kept.length > 0) url.search = [url.search.replace(/^\?/, ""), ...kept].filter((part) => part !== "").join("&");
388
+ return url;
389
+ }
390
+ /** MR2 (d), MR14: relay one upstream response, buffering a recordable one. */
391
+ relay(x, up, kind, finish) {
392
+ const { req, res } = x;
393
+ const status = up.statusCode ?? 502;
394
+ const sessionId = headerOf(req, "mcp-session-id");
395
+ const rpcId = x.rpcId;
396
+ const UPSTREAM_ABORTED_ERROR = "aimock MCP recorder: the upstream response failed";
397
+ let processed = false;
398
+ let clientGone = false;
399
+ res.on("close", () => {
400
+ if (!res.writableFinished) clientGone = true;
401
+ });
402
+ let sseError = null;
403
+ up.on("error", (err) => {
404
+ if (clientGone || res.writableEnded) {
405
+ finish();
406
+ return;
407
+ }
408
+ const answered = processed;
409
+ const recorded = answered && x.recordSkipped === void 0 && x.recordError === void 0;
410
+ processed = true;
411
+ if (!recorded) x.recordSkipped = "upstream-aborted";
412
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: the upstream response failed mid-body (${require_message_text.fixed("upstream-aborted")}) for ${require_message_text.plainText(x.rpcMethod ?? req.method ?? "?")} on mount ${require_message_text.plainText(x.mount)}: ${require_message_text.plainText(err.message)}`));
413
+ if (!res.headersSent) {
414
+ res.writeHead(502, { "Content-Type": "application/json" });
415
+ res.end(JSON.stringify({ error: UPSTREAM_ABORTED_ERROR }));
416
+ finish();
417
+ return;
418
+ }
419
+ if (!answered && sseError !== null && !res.writableEnded) res.write(sseError(), () => res.destroy());
420
+ else res.destroy();
421
+ finish();
422
+ });
423
+ const challenge = String(up.headers["www-authenticate"] ?? "");
424
+ if (status === 401 || status === 403 && /insufficient_scope/i.test(challenge)) {
425
+ up.resume();
426
+ x.recordSkipped = "upstream-auth";
427
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: recording refused (upstream-auth): the upstream answered ${status} for ${require_message_text.plainText(x.rpcMethod ?? req.method ?? "?")} on mount ${require_message_text.plainText(x.mount)}`));
428
+ res.writeHead(502, { "Content-Type": "application/json" });
429
+ res.end(JSON.stringify({
430
+ error: AUTH_FAILURE_ERROR,
431
+ upstreamStatus: status
432
+ }));
433
+ finish();
434
+ return;
435
+ }
436
+ if (sessionId && (status === 404 || req.method === "DELETE" && status >= 200 && status < 300)) this.sessions.delete(sessionId);
437
+ const headers = {};
438
+ for (const [name, value] of Object.entries(up.headers)) {
439
+ if (value === void 0 || HOP_BY_HOP.has(name.toLowerCase())) continue;
440
+ headers[name] = value;
441
+ }
442
+ const ok = status >= 200 && status < 300;
443
+ if (kind === "tools/call" && !ok) this.skip(x, "upstream-status", "tools/call");
444
+ if (kind === "tools/list" && !ok) this.skip(x, "upstream-status", "tools/list");
445
+ if (kind === null || !ok) {
446
+ res.writeHead(status, headers);
447
+ up.pipe(res);
448
+ up.on("end", finish);
449
+ return;
450
+ }
451
+ const isSse = String(up.headers["content-type"] ?? "").includes("text/event-stream");
452
+ const messages = [];
453
+ let size = 0;
454
+ let overflow = false;
455
+ const process = () => {
456
+ if (processed) return;
457
+ processed = true;
458
+ if (overflow) {
459
+ this.skip(x, "buffer-cap", kind);
460
+ return;
461
+ }
462
+ try {
463
+ this.onRecordable(x, kind, messages, up);
464
+ } catch (err) {
465
+ this.fail(x, "record-error", err instanceof Error ? err.name : typeof err);
466
+ }
467
+ };
468
+ if (isSse) {
469
+ res.writeHead(status, headers);
470
+ res.flushHeaders();
471
+ const decoder = new node_string_decoder.StringDecoder("utf8");
472
+ let pending = "";
473
+ if (rpcId !== void 0) {
474
+ const error = {
475
+ code: -32603,
476
+ message: UPSTREAM_ABORTED_ERROR
477
+ };
478
+ sseError = () => {
479
+ return `${pending === "" ? "" : `${/[\r\n]$/.test(pending) ? "" : "\n"}event: aimock-discarded\n\n`}event: message\ndata: ${JSON.stringify({
480
+ jsonrpc: "2.0",
481
+ id: rpcId,
482
+ error
483
+ })}\n\n`;
484
+ };
485
+ }
486
+ up.on("data", (chunk) => {
487
+ if (!processed && !overflow) {
488
+ size += chunk.length;
489
+ if (size > this.maxBuffer) {
490
+ overflow = true;
491
+ process();
492
+ } else {
493
+ pending += decoder.write(chunk);
494
+ const events = pending.split(/\r?\n\r?\n/);
495
+ pending = events.pop() ?? "";
496
+ for (const event of events) for (const message of sseMessages(event)) messages.push({
497
+ at: performance.now() - x.arrivedAt,
498
+ message
499
+ });
500
+ if (messages.some((m) => isResponseTo(m.message, rpcId))) process();
501
+ }
502
+ }
503
+ res.write(chunk);
504
+ });
505
+ up.on("end", () => {
506
+ if (!processed && !overflow) for (const message of sseMessages(pending + decoder.end())) messages.push({
507
+ at: performance.now() - x.arrivedAt,
508
+ message
509
+ });
510
+ process();
511
+ res.end();
512
+ finish();
513
+ });
514
+ return;
515
+ }
516
+ const chunks = [];
517
+ up.on("data", (chunk) => {
518
+ if (overflow) {
519
+ res.write(chunk);
520
+ return;
521
+ }
522
+ size += chunk.length;
523
+ if (size > this.maxBuffer) {
524
+ overflow = true;
525
+ process();
526
+ res.writeHead(status, headers);
527
+ for (const c of chunks) res.write(c);
528
+ res.write(chunk);
529
+ chunks.length = 0;
530
+ return;
531
+ }
532
+ chunks.push(chunk);
533
+ });
534
+ up.on("end", () => {
535
+ if (!overflow) {
536
+ const text = Buffer.concat(chunks).toString("utf8");
537
+ try {
538
+ const parsed = JSON.parse(text);
539
+ for (const message of Array.isArray(parsed) ? parsed : [parsed]) if (isObject(message)) messages.push({
540
+ at: performance.now() - x.arrivedAt,
541
+ message
542
+ });
543
+ } catch {}
544
+ process();
545
+ if (x.recordError !== void 0) headers["X-AIMock-Record-Error"] = require_recorder.sanitizeHeaderValue(x.recordError);
546
+ res.writeHead(status, headers);
547
+ res.end(text === "" ? void 0 : Buffer.concat(chunks));
548
+ } else res.end();
549
+ finish();
550
+ });
551
+ }
552
+ /** A recordable response, once its JSON-RPC response arrived (or its stream ended). */
553
+ onRecordable(x, kind, messages, up) {
554
+ const response = messages.find((m) => isResponseTo(m.message, x.rpcId));
555
+ if (kind === "initialize") {
556
+ this.onInitialize(x, response?.message, up);
557
+ return;
558
+ }
559
+ if (!response) {
560
+ this.skip(x, "upstream-error", kind);
561
+ return;
562
+ }
563
+ const reason = kind === "tools/call" ? callSkipReason(response.message, messages) : isObject(response.message.result) ? null : "upstream-error";
564
+ if (reason !== null) {
565
+ this.skip(x, reason, kind);
566
+ return;
567
+ }
568
+ if (!this.versionRecordable(x)) {
569
+ this.skip(x, "unsupported-version", kind);
570
+ return;
571
+ }
572
+ const identity = x.identity;
573
+ if (identity.testId === null && identity.context === null) {
574
+ x.recordSkipped = "no-scope";
575
+ if (!this.warnedNoScope.has(x.mount)) {
576
+ this.warnedNoScope.add(x.mount);
577
+ this.log().warn(require_message_text.build(require_message_text.msg`MCP-RECORD: forwarded, not recorded: a request on mount ${require_message_text.plainText(x.mount)} has no testId or context, so it has no recording scope (logged once per mount)`));
578
+ }
579
+ return;
580
+ }
581
+ if (this.config.proxyOnly) return;
582
+ if (kind === "tools/list") this.onToolsList(x, response.message);
583
+ else this.recordCall(x, response, messages);
584
+ }
585
+ /** MR3 / AM4: bind what an upstream `initialize` negotiated to its session. */
586
+ onInitialize(x, response, up) {
587
+ const result = response?.result;
588
+ if (!isObject(result)) return;
589
+ const version = typeof result.protocolVersion === "string" ? result.protocolVersion : null;
590
+ const recordable = version !== null && RECORDABLE_VERSIONS.has(version);
591
+ if (!recordable) this.skip(x, "unsupported-version", "initialize");
592
+ const sessionId = headerOf(up, "mcp-session-id");
593
+ if (!sessionId) return;
594
+ const identity = x.identity;
595
+ this.sessions.set(sessionId, {
596
+ protocolVersion: version,
597
+ recordable,
598
+ ...isObject(result.serverInfo) ? { serverInfo: result.serverInfo } : {},
599
+ clientCapabilities: x.params?.capabilities,
600
+ clientInfo: x.params?.clientInfo,
601
+ ...x.supplied.testId && identity.testId !== null ? { testId: identity.testId } : {},
602
+ ...x.supplied.context && identity.context !== null ? { context: identity.context } : {},
603
+ ...x.supplied.undeclared && identity.undeclared !== null ? { undeclared: identity.undeclared } : {}
604
+ });
605
+ }
606
+ /** AM4: false when the session negotiated, or the request names, a version not recorded. */
607
+ versionRecordable(x) {
608
+ const sessionId = headerOf(x.req, "mcp-session-id");
609
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
610
+ if (session && !session.recordable) return false;
611
+ const header = headerOf(x.req, "mcp-protocol-version");
612
+ return header === void 0 || RECORDABLE_VERSIONS.has(header);
613
+ }
614
+ /** MR16: collect a `tools/list` page; write the list when it completes. */
615
+ onToolsList(x, response) {
616
+ const result = response.result;
617
+ if (!Array.isArray(result.tools)) {
618
+ this.skip(x, "upstream-error", "tools/list");
619
+ return;
620
+ }
621
+ const identity = x.identity;
622
+ const key = listKey(x.mount, identity);
623
+ const cursor = x.params?.cursor;
624
+ const next = typeof result.nextCursor === "string" ? result.nextCursor : void 0;
625
+ let tools;
626
+ if (cursor === void 0) {
627
+ if (this.pendingLists.has(key)) this.log().warn(require_message_text.build(require_message_text.msg`MCP-RECORD: forwarded, not recorded (list-incomplete): tools/list on mount ${require_message_text.plainText(x.mount)}`));
628
+ tools = [...result.tools];
629
+ } else {
630
+ const pending = this.pendingLists.get(key);
631
+ if (!pending || pending.next !== cursor) {
632
+ this.skip(x, "list-orphan-page", "tools/list");
633
+ return;
634
+ }
635
+ tools = [...pending.tools, ...result.tools];
636
+ }
637
+ this.pendingLists.delete(key);
638
+ if (next !== void 0) {
639
+ this.pendingLists.set(key, {
640
+ tools,
641
+ next,
642
+ testId: identity.testId
643
+ });
644
+ return;
645
+ }
646
+ const filepath = this.targetFile(identity);
647
+ const scope = scopeOf(identity);
648
+ if (!fileHasBlock(filepath, scope, x.mount)) {
649
+ this.completedLists.set(key, {
650
+ tools,
651
+ testId: identity.testId
652
+ });
653
+ return;
654
+ }
655
+ let list;
656
+ try {
657
+ list = require_record_sanitize.sanitizeRecording(tools, this.secrets(x), isMetaBranch);
658
+ } catch (err) {
659
+ this.failUnsafe(x, err);
660
+ return;
661
+ }
662
+ this.persist(x, filepath, scope, (block) => ({
663
+ block: withList(block ?? {}, list.value),
664
+ warnings: (prefix) => list.warnings.map((w) => `${prefix}/list${w}`)
665
+ }));
666
+ }
667
+ /** MR6 / MR7 / MR10-MR12: record one forwarded `tools/call`. */
668
+ recordCall(x, response, messages) {
669
+ const identity = x.identity;
670
+ const name = String(x.params?.name ?? "");
671
+ const result = cleanResult(response.message.result);
672
+ const entry = { args: x.params?.arguments ?? {} };
673
+ const errorText = errorTextOf(result);
674
+ if (errorText !== null) entry.error = errorText;
675
+ else entry.result = result;
676
+ const notifications = messages.filter((m) => m !== response && m.at <= response.at && !("id" in m.message) && typeof m.message.method === "string" && m.message.method.startsWith("notifications/")).map((m) => ({
677
+ atMs: Math.round(m.at),
678
+ method: m.message.method,
679
+ params: isObject(m.message.params) ? m.message.params : {}
680
+ }));
681
+ if (notifications.length > 0) entry.notifications = notifications;
682
+ entry.durationMs = Math.round(response.at);
683
+ const filepath = this.targetFile(identity);
684
+ const scope = scopeOf(identity);
685
+ const key = listKey(x.mount, identity);
686
+ const waitingList = this.completedLists.get(key);
687
+ const secrets = this.secrets(x);
688
+ let clean;
689
+ let recorded;
690
+ let list = null;
691
+ try {
692
+ clean = require_record_sanitize.sanitizeRecording(entry, secrets, isMetaBranch);
693
+ recorded = require_record_sanitize.sanitizeRecording(this.provenance(x, response.message), secrets, () => true);
694
+ if (waitingList) list = require_record_sanitize.sanitizeRecording(waitingList.tools, secrets, isMetaBranch);
695
+ } catch (err) {
696
+ this.failUnsafe(x, err);
697
+ return;
698
+ }
699
+ if (!this.persist(x, filepath, scope, (block) => {
700
+ if (!block) return {
701
+ block: {
702
+ scope,
703
+ mount: x.mount,
704
+ ...list ? { list: list.value } : {},
705
+ recorded: recorded.value,
706
+ tools: [{
707
+ name,
708
+ calls: [clean.value]
709
+ }]
710
+ },
711
+ warnings: (prefix) => [
712
+ ...list ? list.warnings.map((w) => `${prefix}/list${w}`) : [],
713
+ ...recorded.warnings.map((w) => `${prefix}/recorded${w}`),
714
+ ...clean.warnings.map((w) => `${prefix}/tools/0/calls/0${w}`)
715
+ ]
716
+ };
717
+ const tools = Array.isArray(block.tools) ? [...block.tools] : [];
718
+ let t = tools.findIndex((tool) => isObject(tool) && tool.name === name);
719
+ let c = 0;
720
+ if (t === -1) {
721
+ tools.push({
722
+ name,
723
+ calls: [clean.value]
724
+ });
725
+ t = tools.length - 1;
726
+ } else {
727
+ const tool = tools[t];
728
+ const calls = Array.isArray(tool.calls) ? [...tool.calls] : [];
729
+ calls.push(clean.value);
730
+ c = calls.length - 1;
731
+ tools[t] = {
732
+ ...tool,
733
+ calls
734
+ };
735
+ }
736
+ return {
737
+ block: list ? withList({
738
+ ...block,
739
+ tools
740
+ }, list.value) : {
741
+ ...block,
742
+ tools
743
+ },
744
+ warnings: (prefix) => [...list ? list.warnings.map((w) => `${prefix}/list${w}`) : [], ...clean.warnings.map((w) => `${prefix}/tools/${t}/calls/${c}${w}`)]
745
+ };
746
+ })) return;
747
+ if (waitingList) this.completedLists.delete(key);
748
+ const before = new Set(this.host.fakes.allEntryIds());
749
+ try {
750
+ this.host.addMcpFakes([{
751
+ source: "record",
752
+ blockIndex: null,
753
+ raw: {
754
+ scope,
755
+ mount: x.mount,
756
+ tools: [{
757
+ name,
758
+ calls: [clean.value]
759
+ }]
760
+ }
761
+ }], { kind: "record" });
762
+ } catch (err) {
763
+ const detail = err instanceof Error ? err.message : String(err);
764
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: the recorded entry was written but not registered on mount ${require_message_text.plainText(x.mount)}: ${require_message_text.plainText(detail)}`));
765
+ return;
766
+ }
767
+ const added = this.host.fakes.allEntryIds().filter((id) => !before.has(id));
768
+ this.host.fakes.markConsumed(identity.testId, added);
769
+ }
770
+ /** FA2 `recorded` for a new block (MR6 3). */
771
+ provenance(x, response) {
772
+ const sessionId = headerOf(x.req, "mcp-session-id");
773
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
774
+ const meta = isObject(x.params?._meta) ? x.params._meta : void 0;
775
+ const result = isObject(response.result) ? response.result : void 0;
776
+ const resultMeta = isObject(result?._meta) ? result._meta : void 0;
777
+ const protocolVersion = session?.protocolVersion ?? (typeof meta?.[PROTOCOL_VERSION_META] === "string" ? meta[PROTOCOL_VERSION_META] : void 0) ?? headerOf(x.req, "mcp-protocol-version") ?? FALLBACK_VERSION;
778
+ const serverInfo = session?.serverInfo ?? (isObject(resultMeta?.[SERVER_INFO_META]) ? resultMeta[SERVER_INFO_META] : void 0);
779
+ return {
780
+ upstream: this.target.origin,
781
+ protocolVersion,
782
+ ...serverInfo ? { serverInfo } : {},
783
+ aimockVersion: require_version.aimockVersion((m) => this.log().warn(m)),
784
+ at: (/* @__PURE__ */ new Date()).toISOString()
785
+ };
786
+ }
787
+ /** S2 (a)-(d): the known secrets of one request. */
788
+ secrets(x) {
789
+ const headers = {};
790
+ for (const [name, value] of Object.entries(x.req.headers)) if (value !== void 0) headers[name] = Array.isArray(value) ? value.join(", ") : value;
791
+ return require_record_sanitize.knownSecrets({
792
+ headers,
793
+ ...this.auth ? { upstreamAuth: this.auth } : {},
794
+ upstreamUrl: this.upstreamUrl(x.req, x.subPath).href,
795
+ secretValues: this.secretValues
796
+ });
797
+ }
798
+ /** S6 / RL1: an unsafe recording is not written. */
799
+ failUnsafe(x, err) {
800
+ if (!(err instanceof require_record_sanitize.RecordUnsafeError)) throw err;
801
+ this.fail(x, "unsafe", err.reason);
802
+ }
803
+ /** RL1: a recording that was not written, with its reason (never a value). */
804
+ fail(x, reason, detail) {
805
+ x.recordSkipped = reason;
806
+ x.recordError = `${reason}: ${detail}`;
807
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: recording failed (${require_message_text.fixed(reason)}) for ${require_message_text.plainText(x.rpcMethod ?? "?")} on mount ${require_message_text.plainText(x.mount)}: ${require_message_text.plainText(detail)}`));
808
+ }
809
+ /**
810
+ * MR10 / MR11: merge one change into the block of `scope` and this mount
811
+ * (appending a block when none matches), validate the whole file, and write
812
+ * it. `apply` gets that block (`undefined` when it is new) and returns the
813
+ * new block and its sanitizer warnings under the block's pointer.
814
+ */
815
+ persist(x, filepath, scope, apply) {
816
+ const result = require_recorder.persistServiceFakes({
817
+ filepath,
818
+ logger: this.log(),
819
+ merge: (existing) => mergeBlock(existing, scope, x.mount, apply),
820
+ validate: (content) => {
821
+ const first = require_mcp_fakes.validateMcpFakes(content.mcpFakes, filepath).errors[0];
822
+ if (!first) return null;
823
+ const where = first.entryId ?? first.blockId;
824
+ return where ? `${first.rule} (${where})` : first.rule;
825
+ }
826
+ });
827
+ if (result.kind === "failed") {
828
+ this.fail(x, result.stage === "write" ? "write-failed" : "invalid-merge", result.error);
829
+ return false;
830
+ }
831
+ const resolved = node_path.resolve(result.filepath);
832
+ this.hashes.set(resolved, result.sha256);
833
+ for (const listener of this.listeners) try {
834
+ listener(resolved, result.content.mcpFakes);
835
+ } catch (err) {
836
+ const detail = err instanceof Error ? err.message : String(err);
837
+ this.log().error(require_message_text.build(require_message_text.msg`MCP-RECORD: a write listener failed for ${require_message_text.plainText(resolved)} on mount ${require_message_text.plainText(x.mount)}: ${require_message_text.plainText(detail)}`));
838
+ }
839
+ return true;
840
+ }
841
+ /** MR10: the file a recording of `identity` goes to. */
842
+ targetFile(identity) {
843
+ const slug = identity.testId !== null ? require_helpers.slugifyTestId(identity.testId) : identity.context !== null ? require_helpers.slugifyContext(identity.context) : "";
844
+ if (slug) return node_path.join(this.fixturePath, slug, "mcp.json");
845
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
846
+ return node_path.join(this.fixturePath, `mcp-${timestamp}-${node_crypto.randomUUID().slice(0, 8)}.json`);
847
+ }
848
+ /** MR8 / RL2: mark the request forwarded-not-recorded and warn. */
849
+ skip(x, reason, method) {
850
+ x.recordSkipped = reason;
851
+ this.log().warn(require_message_text.build(require_message_text.msg`MCP-RECORD: forwarded, not recorded (${require_message_text.fixed(reason)}): ${require_message_text.plainText(method)} on mount ${require_message_text.plainText(x.mount)}`));
852
+ }
853
+ log() {
854
+ return this.host.logger() ?? this.fallbackLogger;
855
+ }
856
+ /** AM8: one journal entry per request. */
857
+ journal(x) {
858
+ const { req, res } = x;
859
+ this.host.journalEntry({
860
+ method: req.method ?? "POST",
861
+ path: req.url ?? "/",
862
+ headers: require_helpers.flattenHeaders(req.headers),
863
+ body: x.message,
864
+ service: "mcp",
865
+ ...x.identity ? {
866
+ testId: x.identity.testId ?? require_mcp_fakes.MCP_FAKES_DEFAULT_TEST_ID,
867
+ context: x.identity.context
868
+ } : {},
869
+ response: {
870
+ status: res.statusCode,
871
+ fixture: null,
872
+ ...x.proxied ? { source: "proxy" } : {},
873
+ ...x.recordSkipped !== void 0 ? { recordSkipped: x.recordSkipped } : {},
874
+ ...x.mcpFake ? { mcpFake: x.mcpFake } : {}
875
+ }
876
+ });
877
+ }
878
+ };
879
+ function isObject(value) {
880
+ return typeof value === "object" && value !== null && !Array.isArray(value);
881
+ }
882
+ /** One header's value (the first, when repeated), or undefined. */
883
+ function headerOf(req, name) {
884
+ const value = req.headers[name];
885
+ return Array.isArray(value) ? value[0] : value;
886
+ }
887
+ /** The JSON-RPC messages of one SSE event (its `data:` lines). */
888
+ function sseMessages(event) {
889
+ const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
890
+ if (data === "") return [];
891
+ try {
892
+ const parsed = JSON.parse(data);
893
+ return (Array.isArray(parsed) ? parsed : [parsed]).filter(isObject);
894
+ } catch {
895
+ return [];
896
+ }
897
+ }
898
+ /** Whether `message` is the JSON-RPC response to request `id`. */
899
+ function isResponseTo(message, id) {
900
+ return id !== void 0 && message.id === id && ("result" in message || "error" in message);
901
+ }
902
+ /** The decoded name of one raw `name=value` query pair (form rules: `+` is a space). */
903
+ function queryName(pair) {
904
+ const eq = pair.indexOf("=");
905
+ const name = (eq === -1 ? pair : pair.slice(0, eq)).replace(/\+/g, " ");
906
+ try {
907
+ return decodeURIComponent(name);
908
+ } catch {
909
+ return name;
910
+ }
911
+ }
912
+ function mountOfKey(key) {
913
+ return key.slice(0, key.indexOf("\0"));
914
+ }
915
+ /** MR8: why a forwarded `tools/call` response is not recorded, or null. */
916
+ function callSkipReason(response, messages) {
917
+ if ("error" in response) return "upstream-error";
918
+ const result = response.result;
919
+ if (!isObject(result)) return "upstream-error";
920
+ if (result.resultType === "input_required") return "input-required";
921
+ if (result.task !== void 0 || isObject(result._meta) && RELATED_TASK_META in result._meta) return "task-handle";
922
+ if (messages.some((m) => typeof m.message.method === "string" && "id" in m.message)) return "server-request";
923
+ return null;
924
+ }
925
+ /** MR7: a recorded result without trace `_meta` keys, serverInfo and `resultType: "complete"`. */
926
+ function cleanResult(result) {
927
+ const out = { ...result };
928
+ if (out.resultType === "complete") delete out.resultType;
929
+ if (isObject(out._meta)) {
930
+ const meta = { ...out._meta };
931
+ for (const key of DROPPED_META) delete meta[key];
932
+ if (Object.keys(meta).length > 0) out._meta = meta;
933
+ else delete out._meta;
934
+ }
935
+ return out;
936
+ }
937
+ /** MR6: the `error` text of an `isError` result that is exactly one text block, else null. */
938
+ function errorTextOf(result) {
939
+ if (result.isError !== true) return null;
940
+ if (!Object.keys(result).every((k) => k === "content" || k === "isError")) return null;
941
+ const content = result.content;
942
+ if (!Array.isArray(content) || content.length !== 1) return null;
943
+ const block = content[0];
944
+ if (!isObject(block) || block.type !== "text" || typeof block.text !== "string") return null;
945
+ return Object.keys(block).length === 2 ? block.text : null;
946
+ }
947
+ /** S4: credential-shaped keys are removed only inside `_meta`. */
948
+ function isMetaBranch(pointer) {
949
+ return /(?:^|\/)_meta(?:\/|$)/.test(pointer);
950
+ }
951
+ /** MR10 block scope: the test id, plus the context when there is one. */
952
+ function scopeOf(identity) {
953
+ if (identity.testId === null) return { context: identity.context ?? "" };
954
+ return identity.context === null ? { testId: identity.testId } : {
955
+ testId: identity.testId,
956
+ context: identity.context
957
+ };
958
+ }
959
+ /** MR10: a block scope exactly equal to `scope`. */
960
+ function sameScope(raw, scope) {
961
+ if (!isObject(raw)) return false;
962
+ const keys = Object.keys(scope);
963
+ return Object.keys(raw).length === keys.length && keys.every((k) => raw[k] === scope[k]);
964
+ }
965
+ /** MR16 pending-list key: mount and scope. */
966
+ function listKey(mount, identity) {
967
+ return `${mount}\u0000${JSON.stringify([identity.testId, identity.context])}`;
968
+ }
969
+ /** Whether the file at `filepath` already has the block of `scope` and `mount`. */
970
+ function fileHasBlock(filepath, scope, mount) {
971
+ let parsed;
972
+ try {
973
+ parsed = JSON.parse(node_fs.readFileSync(filepath, "utf-8"));
974
+ } catch {
975
+ return false;
976
+ }
977
+ if (!isObject(parsed)) return false;
978
+ const raw = parsed.mcpFakes;
979
+ return (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).some((b) => isObject(b) && sameScope(b.scope, scope) && b.mount === mount);
980
+ }
981
+ /** MR10 exception: a block with `list` replaced (or inserted after `mount`), key order kept. */
982
+ function withList(block, list) {
983
+ if ("list" in block) return {
984
+ ...block,
985
+ list
986
+ };
987
+ const out = {};
988
+ let placed = false;
989
+ for (const [key, value] of Object.entries(block)) {
990
+ out[key] = value;
991
+ if (key === "mount") {
992
+ out.list = list;
993
+ placed = true;
994
+ }
995
+ }
996
+ if (!placed) out.list = list;
997
+ return out;
998
+ }
999
+ /**
1000
+ * MR10 append rule over a whole file: the first block whose scope and mount
1001
+ * are exactly equal is changed by `apply`; otherwise a block is appended.
1002
+ * Object form becomes array form only when a second block is needed. Other
1003
+ * top-level keys are kept (fakes:F9); `_warnings` gains the new pointers.
1004
+ */
1005
+ function mergeBlock(existing, scope, mount, apply) {
1006
+ const content = existing ? { ...existing } : {};
1007
+ const raw = content.mcpFakes;
1008
+ const blocks = Array.isArray(raw) ? [...raw] : raw === void 0 ? [] : [raw];
1009
+ let index = blocks.findIndex((b) => isObject(b) && sameScope(b.scope, scope) && b.mount === mount);
1010
+ const applied = apply(index === -1 ? void 0 : blocks[index]);
1011
+ if (index === -1) {
1012
+ blocks.push(applied.block);
1013
+ index = blocks.length - 1;
1014
+ } else blocks[index] = applied.block;
1015
+ const arrayForm = Array.isArray(raw) || blocks.length > 1;
1016
+ content.mcpFakes = arrayForm ? blocks : blocks[0];
1017
+ let warnings = Array.isArray(content._warnings) ? content._warnings.filter((w) => typeof w === "string") : [];
1018
+ if (isObject(raw) && arrayForm) warnings = warnings.map((w) => /^\/mcpFakes(?=[/:])/.test(w) ? `/mcpFakes/0${w.slice(9)}` : w);
1019
+ const added = applied.warnings(arrayForm ? `/mcpFakes/${index}` : "/mcpFakes");
1020
+ if (warnings.length > 0 || added.length > 0) content._warnings = [...new Set([...warnings, ...added])];
1021
+ return content;
1022
+ }
1023
+ /**
1024
+ * A mount key as an error message may show it: JSON-quoted, with any
1025
+ * credential (URL userinfo, a credential query parameter, a secret-looking
1026
+ * fragment) removed by the record sanitizer's URL rule (S2 c) and marked as
1027
+ * removed. A key with an `@` that the URL rule cannot parse is not shown.
1028
+ */
1029
+ function redactMountForError(mount) {
1030
+ const prefix = /^[a-z][a-z0-9+.-]*:\/\//i.test(mount) ? "" : mount.startsWith("/") ? "http://localhost" : "http://";
1031
+ const scrubbed = require_record_sanitize.scrubUrl(prefix + mount, () => false, () => {
1032
+ throw new Error("unreachable: no value is a secret here");
1033
+ });
1034
+ if (scrubbed !== prefix + mount) {
1035
+ const shown = scrubbed.startsWith(prefix) ? scrubbed.slice(prefix.length) : require_record_sanitize.REDACTED;
1036
+ return `${JSON.stringify(shown)} (credentials removed)`;
1037
+ }
1038
+ return mount.includes("@") && !URL.canParse(prefix + mount) ? JSON.stringify(require_record_sanitize.REDACTED) : JSON.stringify(mount);
1039
+ }
1040
+ /**
1041
+ * Why a mount path can never be reached, or `undefined` when it can. The
1042
+ * server matches a request's URL pathname against the mount exactly or as a
1043
+ * `<mount>/` prefix, so a mount must be a plain, normalized URL path: it
1044
+ * starts with one `/`, has no trailing `/` (except the root `/`), and has no
1045
+ * query, fragment, whitespace, dot segment or character the URL parser escapes.
1046
+ */
1047
+ function unreachableMountReason(mount) {
1048
+ if (!mount.startsWith("/")) return "it must start with \"/\"";
1049
+ if (mount.startsWith("//")) return "it must not start with \"//\"";
1050
+ if (mount.length > 1 && mount.endsWith("/")) return "it must not end with \"/\"";
1051
+ if (new URL(mount, "http://localhost").pathname !== mount) return "it must be a plain URL path, with no query, fragment, whitespace, dot segment or escaped character";
1052
+ }
1053
+ /**
1054
+ * Why an upstream URL is unusable, or `undefined` when it is a valid http(s)
1055
+ * URL. Never includes the value: it can carry credentials (S2 c).
1056
+ */
1057
+ function badUpstreamReason(upstream) {
1058
+ let url;
1059
+ try {
1060
+ url = new URL(upstream);
1061
+ } catch {
1062
+ return "is not a valid URL";
1063
+ }
1064
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "must be an http: or https: URL";
1065
+ }
1066
+ /**
1067
+ * F2: fail at start, not at every write, when the recording destination can
1068
+ * never be written. The deepest path component that exists must be a
1069
+ * directory this process can write to and enter. A path that does not exist
1070
+ * yet is created on the first write, but not through a broken symbolic link.
1071
+ * `label` prefixes the error (for example the mount it belongs to).
1072
+ */
1073
+ function checkMcpRecordDestination(fixturePath, label) {
1074
+ const destination = node_path.resolve(fixturePath);
1075
+ const fail = (problem) => {
1076
+ const text = `MCP record destination ${destination} ${problem}`;
1077
+ throw new Error(label ? `${label}: ${text}` : text);
1078
+ };
1079
+ const code = (err) => err?.code ?? String(err);
1080
+ let probe = destination;
1081
+ for (;;) {
1082
+ let stat;
1083
+ let broken = false;
1084
+ try {
1085
+ const entry = node_fs.lstatSync(probe, { throwIfNoEntry: false });
1086
+ stat = entry?.isSymbolicLink() ? node_fs.statSync(probe, { throwIfNoEntry: false }) : entry;
1087
+ broken = entry !== void 0 && stat === void 0;
1088
+ } catch (err) {
1089
+ if (code(err) !== "ENOTDIR") fail(`cannot be checked (${code(err)} at ${probe})`);
1090
+ }
1091
+ if (broken) fail(`goes through a broken symbolic link at ${probe}`);
1092
+ if (stat !== void 0) {
1093
+ if (!stat.isDirectory()) fail("is not a directory");
1094
+ try {
1095
+ node_fs.accessSync(probe, node_fs.constants.W_OK | node_fs.constants.X_OK);
1096
+ } catch (err) {
1097
+ fail(`is not writable (${code(err)} at ${probe})`);
1098
+ }
1099
+ return;
1100
+ }
1101
+ const parent = node_path.dirname(probe);
1102
+ if (parent === probe) return;
1103
+ probe = parent;
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Parse a `--mcp-record` / `--mcp-proxy-only` value at its first `=`. The
1108
+ * mount must be a reachable path and the upstream an http(s) URL. A bad value
1109
+ * throws an error that names `flag`, never the URL (it can carry credentials).
1110
+ */
1111
+ function parseMcpRecordFlag(value, flag = "--mcp-record") {
1112
+ const eq = value.indexOf("=");
1113
+ const mount = eq === -1 ? "" : value.slice(0, eq);
1114
+ const upstream = eq === -1 ? "" : value.slice(eq + 1);
1115
+ if (!mount.startsWith("/")) throw new Error(`${flag} must be <mount>=<url>, with a mount that starts with "/"`);
1116
+ const unreachable = unreachableMountReason(mount);
1117
+ if (unreachable !== void 0) throw new Error(`${flag} mount ${redactMountForError(mount)} can never be reached: ${unreachable}`);
1118
+ const badUpstream = badUpstreamReason(upstream);
1119
+ if (badUpstream !== void 0) throw new Error(`${flag} ${mount}: the upstream ${badUpstream}`);
1120
+ return {
1121
+ mount,
1122
+ upstream
1123
+ };
1124
+ }
1125
+ /**
1126
+ * AM6 / S2 (d): the recorder settings that come from the environment.
1127
+ * `AIMOCK_RECORD_SECRET_VALUES` is newline-separated (LF or CRLF); empty
1128
+ * entries are dropped and nothing else is trimmed. A malformed
1129
+ * `AIMOCK_MCP_UPSTREAM_AUTH` throws an error that names the variable.
1130
+ */
1131
+ function mcpRecordEnv(env) {
1132
+ const upstreamAuth = env.AIMOCK_MCP_UPSTREAM_AUTH;
1133
+ parseUpstreamAuth(upstreamAuth);
1134
+ return {
1135
+ secretValues: (env.AIMOCK_RECORD_SECRET_VALUES ?? "").split(/\r?\n/).filter(Boolean),
1136
+ upstreamAuth: upstreamAuth === "" ? void 0 : upstreamAuth
1137
+ };
1138
+ }
1139
+ /**
1140
+ * MR1: `llm.record.mcp` — record each mount path. A path the caller mounted
1141
+ * with an `MCPMock` records through it; a path held by another kind of mount
1142
+ * is a start error; a free path gets a new `MCPMock`, mounted on `llmock`.
1143
+ * A string value is `{ upstream }`. `secretValues` and `upstreamAuth` come
1144
+ * from the value, else from the environment, as on the `llmock` CLI.
1145
+ */
1146
+ function wireMcpRecording(llmock, record, opts) {
1147
+ const where = "llm.record.mcp";
1148
+ const plain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1149
+ const keyTypes = {
1150
+ upstream: "a string",
1151
+ fixturePath: "a non-empty string",
1152
+ proxyOnly: "a boolean",
1153
+ maxRecordBufferBytes: `a finite number greater than 0 and at most ${MAX_RECORD_BUFFER_BYTES}`,
1154
+ upstreamAuth: "a string",
1155
+ secretValues: "an array of strings",
1156
+ strict: "a boolean"
1157
+ };
1158
+ const hasType = (key, v) => {
1159
+ switch (key) {
1160
+ case "proxyOnly":
1161
+ case "strict": return typeof v === "boolean";
1162
+ case "maxRecordBufferBytes": return typeof v === "number" && Number.isFinite(v) && v > 0 && v <= MAX_RECORD_BUFFER_BYTES;
1163
+ case "secretValues": return Array.isArray(v) && v.every((s) => typeof s === "string");
1164
+ case "fixturePath": return typeof v === "string" && v.length > 0;
1165
+ default: return typeof v === "string";
1166
+ }
1167
+ };
1168
+ const isConfigKey = (key) => Object.prototype.hasOwnProperty.call(keyTypes, key);
1169
+ const unknownRecord = record;
1170
+ if (!plain(unknownRecord)) throw new Error(`${where} must be an object of <mount>: <url or record settings>`);
1171
+ const sharedFixturePath = opts.fixturePath;
1172
+ if (sharedFixturePath !== void 0 && !(typeof sharedFixturePath === "string" && sharedFixturePath.length > 0)) throw new Error("llm.record.fixturePath must be a non-empty string");
1173
+ const wired = [];
1174
+ for (const [mountPath, value] of Object.entries(unknownRecord)) {
1175
+ const mount = redactMountForError(mountPath);
1176
+ if (!mountPath.startsWith("/")) throw new Error(`${where} must be <mount>: <url or object>, with a mount that starts with "/"; got ${mount}`);
1177
+ const unreachable = unreachableMountReason(mountPath);
1178
+ if (unreachable !== void 0) throw new Error(`${where} mount ${mount} can never be reached: ${unreachable}`);
1179
+ if (typeof value !== "string" && !plain(value)) throw new Error(`${where} mount ${mount} must be an upstream URL string or an object`);
1180
+ if (typeof value !== "string") {
1181
+ for (const [key, v] of Object.entries(value)) {
1182
+ if (!isConfigKey(key)) throw new Error(`${where} mount ${mount} has an unknown key ${JSON.stringify(key)} (expected one of ${Object.keys(keyTypes).join(", ")})`);
1183
+ if (!hasType(key, v)) throw new Error(`${where} mount ${mount} key ${key} must be ${keyTypes[key]}`);
1184
+ }
1185
+ if (!("upstream" in value)) throw new Error(`${where} mount ${mount} key upstream is required`);
1186
+ }
1187
+ const typed = record[mountPath];
1188
+ const config = typeof typed === "string" ? { upstream: typed } : typed;
1189
+ const badUpstream = badUpstreamReason(config.upstream);
1190
+ if (badUpstream !== void 0) throw new Error(`${where} mount ${mount}: the upstream ${badUpstream}`);
1191
+ const held = opts.mounted.get(mountPath);
1192
+ if (held !== void 0 && !(held instanceof opts.mcpMock)) throw new Error(`llm.record.mcp mount ${mountPath} is held by a mount that is not an MCP mock`);
1193
+ const fixturePath = config.fixturePath ?? opts.fixturePath ?? (opts.fixtures ? node_path.resolve(opts.fixtures, "recorded") : void 0);
1194
+ if (!config.proxyOnly) {
1195
+ if (fixturePath === void 0) throw new Error("llm.record.mcp requires llm.fixtures or llm.record.fixturePath for the recording destination");
1196
+ checkMcpRecordDestination(fixturePath, `${where} mount ${mount}`);
1197
+ }
1198
+ wired.push({
1199
+ mountPath,
1200
+ config,
1201
+ fixturePath,
1202
+ held
1203
+ });
1204
+ }
1205
+ const fromEnv = mcpRecordEnv(opts.env);
1206
+ for (const { mountPath, config, fixturePath, held } of wired) {
1207
+ const mcp = held ?? new opts.mcpMock();
1208
+ mcp.enableRecording({
1209
+ ...config,
1210
+ fixturePath,
1211
+ secretValues: config.secretValues ?? fromEnv.secretValues,
1212
+ upstreamAuth: config.upstreamAuth ?? fromEnv.upstreamAuth,
1213
+ strict: config.strict ?? opts.strict
1214
+ });
1215
+ if (held === void 0) llmock.mount(mountPath, mcp);
1216
+ }
1217
+ }
1218
+
1219
+ //#endregion
1220
+ exports.McpRecorder = McpRecorder;
1221
+ exports.mcpRecordEnv = mcpRecordEnv;
1222
+ exports.parseMcpRecordFlag = parseMcpRecordFlag;
1223
+ exports.wireMcpRecording = wireMcpRecording;
1224
+ //# sourceMappingURL=mcp-recorder.cjs.map