@copilotkit/aimock 1.43.0 → 1.45.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 (396) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +14 -2
  5. package/dist/agui-handler.d.ts.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 +344 -43
  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 +345 -44
  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 +9 -5
  25. package/dist/chaos.cjs.map +1 -1
  26. package/dist/chaos.d.cts.map +1 -1
  27. package/dist/chaos.d.ts.map +1 -1
  28. package/dist/chaos.js +10 -7
  29. package/dist/chaos.js.map +1 -1
  30. package/dist/cli.cjs +238 -9
  31. package/dist/cli.cjs.map +1 -1
  32. package/dist/cli.js +242 -13
  33. package/dist/cli.js.map +1 -1
  34. package/dist/cohere.cjs +159 -72
  35. package/dist/cohere.cjs.map +1 -1
  36. package/dist/cohere.d.cts.map +1 -1
  37. package/dist/cohere.d.ts.map +1 -1
  38. package/dist/cohere.js +160 -73
  39. package/dist/cohere.js.map +1 -1
  40. package/dist/config-loader.cjs +63 -5
  41. package/dist/config-loader.cjs.map +1 -1
  42. package/dist/config-loader.d.cts +37 -4
  43. package/dist/config-loader.d.cts.map +1 -1
  44. package/dist/config-loader.d.ts +37 -4
  45. package/dist/config-loader.d.ts.map +1 -1
  46. package/dist/config-loader.js +63 -5
  47. package/dist/config-loader.js.map +1 -1
  48. package/dist/constants.cjs +8 -0
  49. package/dist/constants.cjs.map +1 -1
  50. package/dist/constants.d.cts +6 -2
  51. package/dist/constants.d.ts +6 -2
  52. package/dist/constants.js +8 -1
  53. package/dist/constants.js.map +1 -1
  54. package/dist/echo-text.cjs +65 -0
  55. package/dist/echo-text.cjs.map +1 -0
  56. package/dist/echo-text.d.cts +22 -0
  57. package/dist/echo-text.d.cts.map +1 -0
  58. package/dist/echo-text.d.ts +22 -0
  59. package/dist/echo-text.d.ts.map +1 -0
  60. package/dist/echo-text.js +65 -0
  61. package/dist/echo-text.js.map +1 -0
  62. package/dist/embeddings.cjs +18 -1
  63. package/dist/embeddings.cjs.map +1 -1
  64. package/dist/embeddings.d.cts +1 -1
  65. package/dist/embeddings.d.cts.map +1 -1
  66. package/dist/embeddings.d.ts +1 -1
  67. package/dist/embeddings.d.ts.map +1 -1
  68. package/dist/embeddings.js +18 -1
  69. package/dist/embeddings.js.map +1 -1
  70. package/dist/fakes-plugin.cjs +131 -0
  71. package/dist/fakes-plugin.cjs.map +1 -0
  72. package/dist/fakes-plugin.d.cts +23 -0
  73. package/dist/fakes-plugin.d.cts.map +1 -0
  74. package/dist/fakes-plugin.d.ts +23 -0
  75. package/dist/fakes-plugin.d.ts.map +1 -0
  76. package/dist/fakes-plugin.js +127 -0
  77. package/dist/fakes-plugin.js.map +1 -0
  78. package/dist/files.cjs +9 -11
  79. package/dist/files.cjs.map +1 -1
  80. package/dist/files.d.cts.map +1 -1
  81. package/dist/files.d.ts.map +1 -1
  82. package/dist/files.js +10 -13
  83. package/dist/files.js.map +1 -1
  84. package/dist/fine-tuning.cjs +1 -19
  85. package/dist/fine-tuning.cjs.map +1 -1
  86. package/dist/fine-tuning.d.cts.map +1 -1
  87. package/dist/fine-tuning.d.ts.map +1 -1
  88. package/dist/fine-tuning.js +2 -20
  89. package/dist/fine-tuning.js.map +1 -1
  90. package/dist/fixture-loader-services.cjs +116 -0
  91. package/dist/fixture-loader-services.cjs.map +1 -0
  92. package/dist/fixture-loader-services.d.cts +48 -0
  93. package/dist/fixture-loader-services.d.cts.map +1 -0
  94. package/dist/fixture-loader-services.d.ts +48 -0
  95. package/dist/fixture-loader-services.d.ts.map +1 -0
  96. package/dist/fixture-loader-services.js +114 -0
  97. package/dist/fixture-loader-services.js.map +1 -0
  98. package/dist/fixture-loader.cjs +552 -29
  99. package/dist/fixture-loader.cjs.map +1 -1
  100. package/dist/fixture-loader.d.cts +140 -2
  101. package/dist/fixture-loader.d.cts.map +1 -1
  102. package/dist/fixture-loader.d.ts +140 -2
  103. package/dist/fixture-loader.d.ts.map +1 -1
  104. package/dist/fixture-loader.js +542 -31
  105. package/dist/fixture-loader.js.map +1 -1
  106. package/dist/gemini-embeddings.cjs +42 -2
  107. package/dist/gemini-embeddings.cjs.map +1 -1
  108. package/dist/gemini-embeddings.js +43 -3
  109. package/dist/gemini-embeddings.js.map +1 -1
  110. package/dist/gemini-interactions.cjs +288 -52
  111. package/dist/gemini-interactions.cjs.map +1 -1
  112. package/dist/gemini-interactions.d.cts.map +1 -1
  113. package/dist/gemini-interactions.d.ts.map +1 -1
  114. package/dist/gemini-interactions.js +289 -53
  115. package/dist/gemini-interactions.js.map +1 -1
  116. package/dist/gemini.cjs +234 -38
  117. package/dist/gemini.cjs.map +1 -1
  118. package/dist/gemini.d.cts.map +1 -1
  119. package/dist/gemini.d.ts.map +1 -1
  120. package/dist/gemini.js +235 -39
  121. package/dist/gemini.js.map +1 -1
  122. package/dist/helpers.cjs +1157 -12
  123. package/dist/helpers.cjs.map +1 -1
  124. package/dist/helpers.d.cts +85 -4
  125. package/dist/helpers.d.cts.map +1 -1
  126. package/dist/helpers.d.ts +85 -4
  127. package/dist/helpers.d.ts.map +1 -1
  128. package/dist/helpers.js +1113 -14
  129. package/dist/helpers.js.map +1 -1
  130. package/dist/images.cjs +3 -2
  131. package/dist/images.cjs.map +1 -1
  132. package/dist/images.js +3 -2
  133. package/dist/images.js.map +1 -1
  134. package/dist/index.cjs +39 -1
  135. package/dist/index.d.cts +11 -4
  136. package/dist/index.d.ts +11 -4
  137. package/dist/index.js +11 -4
  138. package/dist/jest.cjs +43 -14
  139. package/dist/jest.cjs.map +1 -1
  140. package/dist/jest.d.cts +47 -5
  141. package/dist/jest.d.cts.map +1 -1
  142. package/dist/jest.d.ts +47 -5
  143. package/dist/jest.d.ts.map +1 -1
  144. package/dist/jest.js +45 -16
  145. package/dist/jest.js.map +1 -1
  146. package/dist/journal.cjs +155 -13
  147. package/dist/journal.cjs.map +1 -1
  148. package/dist/journal.d.cts +54 -4
  149. package/dist/journal.d.cts.map +1 -1
  150. package/dist/journal.d.ts +54 -4
  151. package/dist/journal.d.ts.map +1 -1
  152. package/dist/journal.js +155 -13
  153. package/dist/journal.js.map +1 -1
  154. package/dist/live-sanitize.cjs +4 -32
  155. package/dist/live-sanitize.cjs.map +1 -1
  156. package/dist/live-sanitize.js +4 -32
  157. package/dist/live-sanitize.js.map +1 -1
  158. package/dist/llmock.cjs +204 -39
  159. package/dist/llmock.cjs.map +1 -1
  160. package/dist/llmock.d.cts +51 -1
  161. package/dist/llmock.d.cts.map +1 -1
  162. package/dist/llmock.d.ts +51 -1
  163. package/dist/llmock.d.ts.map +1 -1
  164. package/dist/llmock.js +206 -41
  165. package/dist/llmock.js.map +1 -1
  166. package/dist/mcp-fakes-mount.cjs +217 -0
  167. package/dist/mcp-fakes-mount.cjs.map +1 -0
  168. package/dist/mcp-fakes-mount.js +211 -0
  169. package/dist/mcp-fakes-mount.js.map +1 -0
  170. package/dist/mcp-fakes-report.cjs +111 -0
  171. package/dist/mcp-fakes-report.cjs.map +1 -0
  172. package/dist/mcp-fakes-report.d.cts +40 -0
  173. package/dist/mcp-fakes-report.d.cts.map +1 -0
  174. package/dist/mcp-fakes-report.d.ts +40 -0
  175. package/dist/mcp-fakes-report.d.ts.map +1 -0
  176. package/dist/mcp-fakes-report.js +107 -0
  177. package/dist/mcp-fakes-report.js.map +1 -0
  178. package/dist/mcp-fakes.cjs +1942 -0
  179. package/dist/mcp-fakes.cjs.map +1 -0
  180. package/dist/mcp-fakes.d.cts +504 -0
  181. package/dist/mcp-fakes.d.cts.map +1 -0
  182. package/dist/mcp-fakes.d.ts +504 -0
  183. package/dist/mcp-fakes.d.ts.map +1 -0
  184. package/dist/mcp-fakes.js +1937 -0
  185. package/dist/mcp-fakes.js.map +1 -0
  186. package/dist/mcp-handler.cjs +534 -27
  187. package/dist/mcp-handler.cjs.map +1 -1
  188. package/dist/mcp-handler.d.cts +49 -0
  189. package/dist/mcp-handler.d.cts.map +1 -0
  190. package/dist/mcp-handler.d.ts +49 -0
  191. package/dist/mcp-handler.d.ts.map +1 -0
  192. package/dist/mcp-handler.js +530 -28
  193. package/dist/mcp-handler.js.map +1 -1
  194. package/dist/mcp-mock.cjs +326 -47
  195. package/dist/mcp-mock.cjs.map +1 -1
  196. package/dist/mcp-mock.d.cts +97 -1
  197. package/dist/mcp-mock.d.cts.map +1 -1
  198. package/dist/mcp-mock.d.ts +97 -1
  199. package/dist/mcp-mock.d.ts.map +1 -1
  200. package/dist/mcp-mock.js +327 -48
  201. package/dist/mcp-mock.js.map +1 -1
  202. package/dist/mcp-recorder.cjs +1224 -0
  203. package/dist/mcp-recorder.cjs.map +1 -0
  204. package/dist/mcp-recorder.d.cts +108 -0
  205. package/dist/mcp-recorder.d.cts.map +1 -0
  206. package/dist/mcp-recorder.d.ts +108 -0
  207. package/dist/mcp-recorder.d.ts.map +1 -0
  208. package/dist/mcp-recorder.js +1215 -0
  209. package/dist/mcp-recorder.js.map +1 -0
  210. package/dist/mcp-stub.cjs +5 -1
  211. package/dist/mcp-stub.d.cts +3 -1
  212. package/dist/mcp-stub.d.ts +3 -1
  213. package/dist/mcp-stub.js +3 -1
  214. package/dist/mcp-types.d.cts +6 -0
  215. package/dist/mcp-types.d.ts +6 -0
  216. package/dist/message-text.cjs +192 -0
  217. package/dist/message-text.cjs.map +1 -0
  218. package/dist/message-text.d.cts +31 -0
  219. package/dist/message-text.d.cts.map +1 -0
  220. package/dist/message-text.d.ts +31 -0
  221. package/dist/message-text.d.ts.map +1 -0
  222. package/dist/message-text.js +184 -0
  223. package/dist/message-text.js.map +1 -0
  224. package/dist/messages.cjs +400 -56
  225. package/dist/messages.cjs.map +1 -1
  226. package/dist/messages.d.cts.map +1 -1
  227. package/dist/messages.d.ts.map +1 -1
  228. package/dist/messages.js +401 -58
  229. package/dist/messages.js.map +1 -1
  230. package/dist/metrics.cjs +46 -66
  231. package/dist/metrics.cjs.map +1 -1
  232. package/dist/metrics.d.cts +14 -6
  233. package/dist/metrics.d.cts.map +1 -1
  234. package/dist/metrics.d.ts +14 -6
  235. package/dist/metrics.d.ts.map +1 -1
  236. package/dist/metrics.js +32 -40
  237. package/dist/metrics.js.map +1 -1
  238. package/dist/misbehavior.cjs +1523 -0
  239. package/dist/misbehavior.cjs.map +1 -0
  240. package/dist/misbehavior.d.cts +43 -0
  241. package/dist/misbehavior.d.cts.map +1 -0
  242. package/dist/misbehavior.d.ts +43 -0
  243. package/dist/misbehavior.d.ts.map +1 -0
  244. package/dist/misbehavior.js +1511 -0
  245. package/dist/misbehavior.js.map +1 -0
  246. package/dist/ollama.cjs +315 -113
  247. package/dist/ollama.cjs.map +1 -1
  248. package/dist/ollama.d.cts.map +1 -1
  249. package/dist/ollama.d.ts.map +1 -1
  250. package/dist/ollama.js +316 -114
  251. package/dist/ollama.js.map +1 -1
  252. package/dist/openapi.cjs +3405 -0
  253. package/dist/openapi.cjs.map +1 -0
  254. package/dist/openapi.d.cts +30 -0
  255. package/dist/openapi.d.cts.map +1 -0
  256. package/dist/openapi.d.ts +30 -0
  257. package/dist/openapi.d.ts.map +1 -0
  258. package/dist/openapi.js +3404 -0
  259. package/dist/openapi.js.map +1 -0
  260. package/dist/openrouter-video.cjs +4 -4
  261. package/dist/openrouter-video.cjs.map +1 -1
  262. package/dist/openrouter-video.d.cts.map +1 -1
  263. package/dist/openrouter-video.d.ts.map +1 -1
  264. package/dist/openrouter-video.js +4 -4
  265. package/dist/openrouter-video.js.map +1 -1
  266. package/dist/record-sanitize.cjs +231 -0
  267. package/dist/record-sanitize.cjs.map +1 -0
  268. package/dist/record-sanitize.js +224 -0
  269. package/dist/record-sanitize.js.map +1 -0
  270. package/dist/recorder.cjs +115 -6
  271. package/dist/recorder.cjs.map +1 -1
  272. package/dist/recorder.d.cts +2 -1
  273. package/dist/recorder.d.cts.map +1 -1
  274. package/dist/recorder.d.ts +2 -1
  275. package/dist/recorder.d.ts.map +1 -1
  276. package/dist/recorder.js +116 -8
  277. package/dist/recorder.js.map +1 -1
  278. package/dist/responses.cjs +701 -166
  279. package/dist/responses.cjs.map +1 -1
  280. package/dist/responses.d.cts +5 -13
  281. package/dist/responses.d.cts.map +1 -1
  282. package/dist/responses.d.ts +5 -13
  283. package/dist/responses.d.ts.map +1 -1
  284. package/dist/responses.js +699 -169
  285. package/dist/responses.js.map +1 -1
  286. package/dist/route-registry.cjs +951 -0
  287. package/dist/route-registry.cjs.map +1 -0
  288. package/dist/route-registry.d.cts +33 -0
  289. package/dist/route-registry.d.cts.map +1 -0
  290. package/dist/route-registry.d.ts +33 -0
  291. package/dist/route-registry.d.ts.map +1 -0
  292. package/dist/route-registry.js +866 -0
  293. package/dist/route-registry.js.map +1 -0
  294. package/dist/router.cjs +24 -2
  295. package/dist/router.cjs.map +1 -1
  296. package/dist/router.d.cts.map +1 -1
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +25 -4
  299. package/dist/router.js.map +1 -1
  300. package/dist/server.cjs +1159 -264
  301. package/dist/server.cjs.map +1 -1
  302. package/dist/server.d.cts +4 -1
  303. package/dist/server.d.cts.map +1 -1
  304. package/dist/server.d.ts +4 -1
  305. package/dist/server.d.ts.map +1 -1
  306. package/dist/server.js +1067 -172
  307. package/dist/server.js.map +1 -1
  308. package/dist/speech.cjs +4 -2
  309. package/dist/speech.cjs.map +1 -1
  310. package/dist/speech.js +4 -2
  311. package/dist/speech.js.map +1 -1
  312. package/dist/stream-collapse.cjs +236 -34
  313. package/dist/stream-collapse.cjs.map +1 -1
  314. package/dist/stream-collapse.d.cts +39 -4
  315. package/dist/stream-collapse.d.cts.map +1 -1
  316. package/dist/stream-collapse.d.ts +39 -4
  317. package/dist/stream-collapse.d.ts.map +1 -1
  318. package/dist/stream-collapse.js +235 -35
  319. package/dist/stream-collapse.js.map +1 -1
  320. package/dist/suite.cjs +1 -1
  321. package/dist/suite.js +1 -1
  322. package/dist/transcription.cjs +2 -2
  323. package/dist/transcription.cjs.map +1 -1
  324. package/dist/transcription.d.cts.map +1 -1
  325. package/dist/transcription.d.ts.map +1 -1
  326. package/dist/transcription.js +2 -2
  327. package/dist/transcription.js.map +1 -1
  328. package/dist/types.cjs +18 -0
  329. package/dist/types.cjs.map +1 -0
  330. package/dist/types.d.cts +516 -5
  331. package/dist/types.d.cts.map +1 -1
  332. package/dist/types.d.ts +516 -5
  333. package/dist/types.d.ts.map +1 -1
  334. package/dist/types.js +17 -0
  335. package/dist/types.js.map +1 -0
  336. package/dist/validate-cli.cjs +123 -33
  337. package/dist/validate-cli.cjs.map +1 -1
  338. package/dist/validate-cli.js +125 -35
  339. package/dist/validate-cli.js.map +1 -1
  340. package/dist/vector-stores.cjs +1180 -0
  341. package/dist/vector-stores.cjs.map +1 -0
  342. package/dist/vector-stores.d.cts +106 -0
  343. package/dist/vector-stores.d.cts.map +1 -0
  344. package/dist/vector-stores.d.ts +106 -0
  345. package/dist/vector-stores.d.ts.map +1 -0
  346. package/dist/vector-stores.js +1164 -0
  347. package/dist/vector-stores.js.map +1 -0
  348. package/dist/vector-types.d.cts.map +1 -1
  349. package/dist/version.cjs +45 -0
  350. package/dist/version.cjs.map +1 -0
  351. package/dist/version.js +44 -0
  352. package/dist/version.js.map +1 -0
  353. package/dist/video.cjs +19 -0
  354. package/dist/video.cjs.map +1 -1
  355. package/dist/video.d.cts.map +1 -1
  356. package/dist/video.d.ts.map +1 -1
  357. package/dist/video.js +19 -0
  358. package/dist/video.js.map +1 -1
  359. package/dist/vitest.cjs +77 -16
  360. package/dist/vitest.cjs.map +1 -1
  361. package/dist/vitest.d.cts +38 -5
  362. package/dist/vitest.d.cts.map +1 -1
  363. package/dist/vitest.d.ts +38 -5
  364. package/dist/vitest.d.ts.map +1 -1
  365. package/dist/vitest.js +78 -17
  366. package/dist/vitest.js.map +1 -1
  367. package/dist/watcher.cjs +6 -2
  368. package/dist/watcher.cjs.map +1 -1
  369. package/dist/watcher.js +6 -2
  370. package/dist/watcher.js.map +1 -1
  371. package/dist/ws-gemini-live.cjs +210 -101
  372. package/dist/ws-gemini-live.cjs.map +1 -1
  373. package/dist/ws-gemini-live.d.cts +9 -3
  374. package/dist/ws-gemini-live.d.cts.map +1 -1
  375. package/dist/ws-gemini-live.d.ts +9 -3
  376. package/dist/ws-gemini-live.d.ts.map +1 -1
  377. package/dist/ws-gemini-live.js +211 -102
  378. package/dist/ws-gemini-live.js.map +1 -1
  379. package/dist/ws-realtime.cjs +175 -62
  380. package/dist/ws-realtime.cjs.map +1 -1
  381. package/dist/ws-realtime.d.cts +8 -3
  382. package/dist/ws-realtime.d.cts.map +1 -1
  383. package/dist/ws-realtime.d.ts +8 -3
  384. package/dist/ws-realtime.d.ts.map +1 -1
  385. package/dist/ws-realtime.js +176 -63
  386. package/dist/ws-realtime.js.map +1 -1
  387. package/dist/ws-responses.cjs +121 -16
  388. package/dist/ws-responses.cjs.map +1 -1
  389. package/dist/ws-responses.d.cts +8 -12
  390. package/dist/ws-responses.d.cts.map +1 -1
  391. package/dist/ws-responses.d.ts +8 -12
  392. package/dist/ws-responses.d.ts.map +1 -1
  393. package/dist/ws-responses.js +123 -18
  394. package/dist/ws-responses.js.map +1 -1
  395. package/package.json +13 -2
  396. package/skills/write-fixtures/SKILL.md +179 -21
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-handler.js","names":[],"sources":["../src/mcp-handler.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { createJsonRpcDispatcher } from \"./jsonrpc.js\";\nimport type {\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\n\nexport interface MCPState {\n serverInfo: { name: string; version: string };\n tools: Map<string, { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }>;\n resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }>;\n prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n >;\n sessions: Map<string, MCPSession>;\n}\n\nfunction jsonRpcResult(id: string | number, result: unknown) {\n return { jsonrpc: \"2.0\" as const, id, result };\n}\n\nfunction jsonRpcError(id: string | number | null, code: number, message: string) {\n return { jsonrpc: \"2.0\" as const, id, error: { code, message } };\n}\n\nexport function createMCPRequestHandler(state: MCPState) {\n const dispatcher = createJsonRpcDispatcher({\n methods: {\n // initialize is handled directly in the outer function — this entry is\n // only here so the dispatcher doesn't return \"Method not found\" if the\n // request somehow reaches it.\n initialize: async (_params, id) => {\n return jsonRpcResult(id, {\n protocolVersion: \"2025-03-26\",\n capabilities: { tools: {}, resources: {}, prompts: {} },\n serverInfo: state.serverInfo,\n });\n },\n\n \"notifications/initialized\": async (_params, _id, req) => {\n const sessionId = req.headers[\"mcp-session-id\"] as string;\n const session = state.sessions.get(sessionId);\n if (session) {\n session.initialized = true;\n }\n return null;\n },\n\n ping: async (_params, id) => {\n return jsonRpcResult(id, {});\n },\n\n \"tools/list\": async (_params, id) => {\n const tools: MCPToolDefinition[] = [];\n for (const { def } of state.tools.values()) {\n tools.push(def);\n }\n return jsonRpcResult(id, { tools });\n },\n\n \"tools/call\": async (params, id) => {\n const { name, arguments: args } = (params ?? {}) as { name?: string; arguments?: unknown };\n if (!name) {\n return jsonRpcError(id, -32602, \"Missing tool name\");\n }\n const entry = state.tools.get(name);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown tool: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n const content: MCPContent[] = Array.isArray(result)\n ? result\n : [{ type: \"text\", text: String(result) }];\n return jsonRpcResult(id, { content, isError: false });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcResult(id, {\n content: [{ type: \"text\", text: message }],\n isError: true,\n });\n }\n }\n // No handler — return empty content\n return jsonRpcResult(id, { content: [], isError: false });\n },\n\n \"resources/list\": async (_params, id) => {\n const resources: MCPResourceDefinition[] = [];\n for (const { def } of state.resources.values()) {\n resources.push(def);\n }\n return jsonRpcResult(id, { resources });\n },\n\n \"resources/read\": async (params, id) => {\n const { uri } = (params ?? {}) as { uri?: string };\n if (!uri) {\n return jsonRpcError(id, -32602, \"Missing resource URI\");\n }\n const entry = state.resources.get(uri);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);\n }\n return jsonRpcResult(id, {\n contents: [\n {\n uri,\n ...(entry.content?.text !== undefined && { text: entry.content.text }),\n ...(entry.content?.blob !== undefined && { blob: entry.content.blob }),\n ...(entry.content?.mimeType !== undefined && { mimeType: entry.content.mimeType }),\n },\n ],\n });\n },\n\n \"prompts/list\": async (_params, id) => {\n const prompts: MCPPromptDefinition[] = [];\n for (const { def } of state.prompts.values()) {\n prompts.push(def);\n }\n return jsonRpcResult(id, { prompts });\n },\n\n \"prompts/get\": async (params, id) => {\n const { name, arguments: args } = (params ?? {}) as { name?: string; arguments?: unknown };\n if (!name) {\n return jsonRpcError(id, -32602, \"Missing prompt name\");\n }\n const entry = state.prompts.get(name);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown prompt: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n return jsonRpcResult(id, result);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcError(id, -32603, `Prompt handler error: ${message}`);\n }\n }\n // No handler — return empty messages\n return jsonRpcResult(id, { messages: [] });\n },\n },\n });\n\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n ): Promise<void> => {\n // DELETE handler: session teardown\n if (req.method === \"DELETE\") {\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return;\n }\n state.sessions.delete(sessionId);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ ok: true }));\n return;\n }\n\n // Parse the body to determine method for session validation\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n // Let the dispatcher handle parse errors\n await dispatcher(req, res, body);\n return;\n }\n\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? (parsed as { method: unknown }).method\n : undefined;\n\n // Handle initialize directly to control response headers\n if (method === \"initialize\") {\n const id =\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? (parsed as { id: unknown }).id\n : null;\n\n const sessionId = randomUUID();\n state.sessions.set(sessionId, {\n id: sessionId,\n initialized: false,\n createdAt: Date.now(),\n });\n\n const response = {\n jsonrpc: \"2.0\",\n id,\n result: {\n protocolVersion: \"2025-03-26\",\n capabilities: { tools: {}, resources: {}, prompts: {} },\n serverInfo: state.serverInfo,\n },\n };\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Mcp-Session-Id\": sessionId,\n });\n res.end(JSON.stringify(response));\n return;\n }\n\n // Session validation for all other methods\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return;\n }\n\n // Enforce initialization: only allow notifications/initialized through\n // before the session is fully initialized\n const session = state.sessions.get(sessionId)!;\n if (!session.initialized && method !== \"notifications/initialized\") {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify(\n jsonRpcError(\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? ((parsed as { id: unknown }).id as string | number)\n : null,\n -32002,\n \"Session not initialized\",\n ),\n ),\n );\n return;\n }\n\n // Delegate to the JSON-RPC dispatcher for all other methods\n await dispatcher(req, res, body);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-handler.js","names":[],"sources":["../src/mcp-handler.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { createJsonRpcDispatcher } from \"./jsonrpc.js\";\nimport type {\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport type { McpFakeClaim, McpFakeStore } from \"./mcp-fakes.js\";\nimport {\n MCP_FAKE_ERROR_CODES,\n type McpFakeErrorCode,\n type McpFakeIdentity,\n type McpFakeNotification,\n type McpFakeOutcome,\n} from \"./types.js\";\nimport { decodeMcpHeaderValue, resolveMcpIdentity, type McpIdentityResolution } from \"./helpers.js\";\nimport { Message, build, fixed, jsonText, msg, plainText, quote } from \"./message-text.js\";\n\n/**\n * `data.aimock.code` of a `tools/call` whose fake claim threw: an aimock\n * defect (a validated entry whose result could not be built or sent), not a\n * scenario failure. Answered with the JSON-RPC internal-error code `-32603`;\n * the entry stays unconsumed.\n */\nexport const MCP_FAKE_INTERNAL_ERROR = \"MCP_FAKE_INTERNAL_ERROR\";\nconst MCP_FAKE_INTERNAL_ERROR_RPC_CODE = -32603;\n\n/** What every fake event carries: who called which tool on which mount (I9). */\ninterface McpFakeEventBase {\n identity: McpFakeIdentity;\n tool: string;\n /** The mount path the request came in on (I9). */\n mount: string;\n /** The call's `arguments` as received (RP2). */\n args: unknown;\n}\n\n/**\n * One MCP fake outcome of a `tools/call`, for the mount's side channels\n * (metric and log line; spec 9.3). `answered` names the entry; every other\n * kind is a failure with its `data.aimock.code` and the JSON-RPC error\n * message sent to the client. `evicted` also carries the evicted test id and\n * the cap; `internal_error` is a claim that threw (`MCP_FAKE_INTERNAL_ERROR`).\n */\nexport type McpFakeEvent = McpFakeEventBase &\n (\n | { kind: \"answered\"; entryId: string }\n | {\n kind: Exclude<McpFakeOutcome, \"answered\" | \"evicted\">;\n code: McpFakeErrorCode;\n message: string;\n }\n | { kind: \"evicted\"; code: \"MCP_FAKE_EVICTED\"; message: string; testId: string; cap: number }\n | { kind: \"internal_error\"; code: typeof MCP_FAKE_INTERNAL_ERROR; message: string }\n );\n\n/** An identity field whose value can fall back to the raw value (H1). */\nexport type McpDecodeField = \"testId\" | \"context\";\n\n/**\n * A decode fallback of one request, for L2: a test id or context value that\n * is not valid percent-encoding (on the header or the query; `used` is false\n * for a query value the header overrode), or query names that are not valid\n * percent-encoding and so match no identity field.\n */\nexport type McpDecodeFallback =\n | { kind: \"value\"; field: McpDecodeField; source: \"header\" | \"query\"; raw: string; used: boolean }\n | { kind: \"names\"; count: number };\n\n/** One JSON-RPC message of a request, for one journal entry (B2). */\nexport interface MCPMessageRecord {\n /** The JSON-RPC request object of a `tools/call`, else `null`. */\n body: Record<string, unknown> | null;\n /** Set when a fake answered or failed this `tools/call`. */\n mcpFake: { id: string | null; outcome: McpFakeOutcome } | null;\n /** What the mount's own code threw while serving this message, else `null`. */\n error: string | null;\n}\n\n/**\n * What one request handled by `createMCPRequestHandler` resolved, for the\n * mount's journal (B2, B3). `identity` is `null` when the request had none to\n * resolve (a rejected identity; a DELETE with a missing or unknown session).\n * `messages` has one record per JSON-RPC message: one for a single message,\n * one per element of a batch, so each `tools/call` of a batch is journaled\n * with its own body and fake outcome. The mount writes one journal entry per\n * record, and one with no body when there is none (a request answered before\n * any message was read, or an empty batch).\n */\nexport interface MCPRequestRecord {\n identity: McpFakeIdentity | null;\n messages: MCPMessageRecord[];\n}\n\n/** An empty record, for the caller to pass in and read even if the handler throws. */\nexport function newMCPRequestRecord(): MCPRequestRecord {\n return { identity: null, messages: [] };\n}\n\nexport interface MCPState {\n serverInfo: { name: string; version: string };\n tools: Map<string, { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }>;\n resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }>;\n prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n >;\n sessions: Map<string, MCPSession>;\n /** The mount's fakes (one store per mount). */\n fakes: McpFakeStore;\n /** Whether any fake block is loaded on the mount (gates L2). */\n hasFakes(): boolean;\n /** A fake answered or failed a `tools/call` (metric, L1/L10 log). */\n onFakeEvent(evt: McpFakeEvent): void;\n /**\n * A request of `sessionId` sent an identity value (or query name) that is\n * not valid percent-encoding (H1). Called only when `hasFakes()`; the mount\n * logs L2 once per session per field and source.\n */\n onDecodeFallback(sessionId: string, fallback: McpDecodeFallback): void;\n /** `sessionId` was deleted (DELETE): the mount drops its per-session state. */\n onSessionClosed(sessionId: string): void;\n /** RP2: a tools/call no fake applied to (fakes:10 step 3), with who answered. */\n onUnfaked(evt: {\n identity: McpFakeIdentity;\n tool: string;\n mount: string;\n args: unknown;\n answeredBy: \"handler\" | \"config\" | \"empty\" | \"unknown-tool\";\n }): void;\n /** T1: the replay speed recorded timing plays at (`value / speed`). */\n replaySpeed(): number;\n}\n\n/**\n * C8: the `onToolCall` handlers that answer a config `mcp.tools[].result`.\n * `tools/call` reports such an answer as `answeredBy: \"config\"`. Internal:\n * not exported from the package.\n */\nexport const configToolHandlers = new WeakSet<(...args: unknown[]) => unknown>();\n\n/** C7: the syslog levels of `logging/setLevel`, lowest first. */\nconst LOG_LEVELS = [\n \"debug\",\n \"info\",\n \"notice\",\n \"warning\",\n \"error\",\n \"critical\",\n \"alert\",\n \"emergency\",\n] as const;\n\n/** C7: the rank of a syslog level name, or -1 when it is not one. */\nfunction logLevelRank(level: unknown): number {\n return typeof level === \"string\" ? (LOG_LEVELS as readonly string[]).indexOf(level) : -1;\n}\n\nfunction jsonRpcResult(id: string | number, result: unknown) {\n return { jsonrpc: \"2.0\" as const, id, result };\n}\n\nfunction jsonRpcError(id: string | number | null, code: number, message: string, data?: unknown) {\n return {\n jsonrpc: \"2.0\" as const,\n id,\n error: data === undefined ? { code, message } : { code, message, data },\n };\n}\n\n/**\n * The identity part of a fake message or log line (I9): `testId \"<id>\"`,\n * `context \"<ctx>\"`, both, or `no testId or context`.\n */\nexport function describeMcpIdentity(identity: McpFakeIdentity): Message {\n const { testId, context } = identity;\n if (testId !== null && context !== null) {\n return msg`testId ${quote(testId)}, context ${quote(context)}`;\n }\n if (testId !== null) return msg`testId ${quote(testId)}`;\n if (context !== null) return msg`context ${quote(context)}`;\n return msg`no testId or context`;\n}\n\n/** A value as JSON text for a message part. */\nfunction jsonPart(value: unknown): ReturnType<typeof jsonText> {\n return jsonText(JSON.stringify(value) ?? \"null\");\n}\n\n/** The outcome of one `tools/call` invocation, for its message record. */\ntype CallOutcome = Omit<MCPMessageRecord, \"body\">;\n\n/**\n * What `tools/call` reads of one request (its resolved identity and mount,\n * and which invocations are notifications) and writes back: one outcome per\n * `tools/call` invocation, in order, so a batch's calls each keep their own.\n */\ninterface FakeRequestContext {\n identity: McpFakeIdentity;\n mount: string;\n /**\n * Per `tools/call` invocation, in order: true when its message has no `id`\n * member. The dispatcher passes a null id both for a notification and for\n * a request whose id is null or not a string or number, so the handler\n * cannot tell them apart from its id.\n */\n notifications: boolean[];\n calls: CallOutcome[];\n /**\n * Set by the outer handler for a single `tools/call` it already claimed\n * (MR15 interception), so the method does not claim twice: `reply` is the\n * answer or failure `callFake` built, `none` means no fake applied. The\n * outcome of that call is already in `calls`.\n */\n precomputed?:\n | {\n kind: \"reply\";\n value: ReturnType<typeof jsonRpcResult> | ReturnType<typeof jsonRpcError>;\n }\n | { kind: \"none\" };\n}\n\n/**\n * A fake failure: the JSON-RPC error to send and the event to report; an\n * `evicted` one also carries the evicted test id and the cap.\n */\ntype FakeFailure = { message: string; aimock: Record<string, unknown> } & (\n | { outcome: Exclude<McpFakeOutcome, \"answered\" | \"evicted\">; code: McpFakeErrorCode }\n | { outcome: \"evicted\"; code: \"MCP_FAKE_EVICTED\"; evicted: { testId: string; cap: number } }\n);\n\n/**\n * The 9.2 error for a failed claim (`mismatch`, `exhausted`, `evicted`), or\n * for an undeclared tool under `deny` (`not_declared`).\n */\nexport function fakeFailure(\n claim: Exclude<McpFakeClaim, { kind: \"answer\" } | { kind: \"none\" }> | { kind: \"not_declared\" },\n tool: string,\n ctx: FakeRequestContext,\n declaredTools: () => string[],\n): FakeFailure {\n const who = describeMcpIdentity(ctx.identity);\n const base = {\n tool,\n testId: ctx.identity.testId,\n context: ctx.identity.context,\n mount: ctx.mount,\n };\n switch (claim.kind) {\n case \"mismatch\": {\n const declared = claim.declared.map((d) =>\n d.args !== undefined\n ? msg`${plainText(d.id)} ${jsonPart(d.args)}`\n : msg`${plainText(d.id)} anyArgs`,\n );\n const list = declared.reduce<Message>(\n (acc, part, i) => (i === 0 ? part : msg`${acc}; ${part}`),\n msg`none`,\n );\n return {\n code: \"MCP_FAKE_MISMATCH\",\n outcome: \"mismatch\",\n message: build(\n msg`aimock MCP fake mismatch: tool ${quote(tool)} was called with arguments that match no declared fake (${who}). Received ${jsonPart(claim.received)}. Declared: ${list}.`,\n ),\n aimock: {\n code: \"MCP_FAKE_MISMATCH\",\n ...base,\n received: claim.received,\n declared: claim.declared,\n firstDifference: claim.firstDifference,\n },\n };\n }\n case \"exhausted\": {\n const used =\n claim.matchingDeclared === 1\n ? msg`the 1 matching fake is`\n : msg`all ${claim.matchingDeclared} matching fakes are`;\n return {\n code: \"MCP_FAKE_EXHAUSTED\",\n outcome: \"exhausted\",\n message: build(\n msg`aimock MCP fake exhausted: tool ${quote(tool)} with ${jsonPart(claim.received)} was called again, but ${used} already used (${who}).`,\n ),\n aimock: {\n code: \"MCP_FAKE_EXHAUSTED\",\n ...base,\n received: claim.received,\n matchingDeclared: claim.matchingDeclared,\n matchingConsumed: claim.matchingConsumed,\n matchingIds: claim.matchingIds,\n },\n };\n }\n case \"evicted\":\n // Only a test id is ever evicted (never the untagged state), so the\n // claim's test id is the request's.\n return {\n code: \"MCP_FAKE_EVICTED\",\n outcome: \"evicted\",\n evicted: { testId: claim.testId, cap: claim.maxTestIds },\n message: build(\n msg`aimock MCP fake evicted: consumption state for testId ${quote(claim.testId)} was evicted by the per-mount test-id cap (${claim.maxTestIds}); reset before reusing this test id (${who}).`,\n ),\n aimock: { code: \"MCP_FAKE_EVICTED\", ...base, testId: claim.testId, cap: claim.maxTestIds },\n };\n case \"not_declared\":\n return {\n code: \"MCP_FAKE_NOT_DECLARED\",\n outcome: \"not_declared\",\n message: build(\n msg`Unknown tool: ${plainText(tool)} (aimock MCP fake not declared: this scenario denies undeclared tools; ${who}).`,\n ),\n aimock: { code: \"MCP_FAKE_NOT_DECLARED\", ...base, declaredTools: declaredTools() },\n };\n }\n}\n\nexport function createMCPRequestHandler(state: MCPState) {\n /** Per-request fake context, set by the outer handler for the dispatcher's methods. */\n const contexts = new WeakMap<http.IncomingMessage, FakeRequestContext>();\n /** C7: each session's `logging/setLevel` level; dropped on DELETE. */\n const logLevels = new Map<string, string>();\n\n /** The initialize capabilities: `logging` only on a mount with recorded logs (C7). */\n const capabilities = (): Record<string, unknown> =>\n state.fakes.hasRecordedLogs()\n ? { tools: {}, resources: {}, prompts: {}, logging: {} }\n : { tools: {}, resources: {}, prompts: {} };\n\n /**\n * Spec 10 steps 1-2 for `tools/call`: a fake answer or fake failure, or\n * `null` to fall through to today's behavior (step 3).\n */\n const callFake = (\n ctx: FakeRequestContext,\n outcome: CallOutcome,\n tool: string,\n args: unknown,\n id: string | number,\n ): ReturnType<typeof jsonRpcResult> | ReturnType<typeof jsonRpcError> | null => {\n const event = { identity: ctx.identity, tool, mount: ctx.mount, args };\n let claim: McpFakeClaim;\n try {\n claim = state.fakes.claim(tool, args, ctx.identity);\n } catch (err) {\n // A validated entry whose result cannot be built or sent: an aimock\n // defect. Fail loud on every side channel, never a generic -32603.\n const thrown = err instanceof Error ? err.message : String(err);\n const message = build(\n msg`aimock MCP fake internal error: tool ${quote(tool)} could not be answered (${describeMcpIdentity(ctx.identity)}): ${plainText(thrown)}`,\n );\n outcome.error = message;\n state.onFakeEvent({\n ...event,\n kind: \"internal_error\",\n code: MCP_FAKE_INTERNAL_ERROR,\n message,\n });\n return jsonRpcError(id, MCP_FAKE_INTERNAL_ERROR_RPC_CODE, message, {\n aimock: {\n code: MCP_FAKE_INTERNAL_ERROR,\n tool,\n testId: ctx.identity.testId,\n context: ctx.identity.context,\n mount: ctx.mount,\n error: thrown,\n },\n });\n }\n if (claim.kind === \"answer\") {\n outcome.mcpFake = { id: claim.entry.id, outcome: \"answered\" };\n state.onFakeEvent({ ...event, kind: \"answered\", entryId: claim.entry.id });\n return jsonRpcResult(id, claim.result);\n }\n let failure: FakeFailure;\n if (claim.kind === \"none\") {\n if (state.fakes.policy(ctx.identity) !== \"deny\") return null;\n failure = fakeFailure({ kind: \"not_declared\" }, tool, ctx, () =>\n state.fakes.declaredTools(ctx.identity),\n );\n } else {\n failure = fakeFailure(claim, tool, ctx, () => []);\n }\n outcome.mcpFake = { id: null, outcome: failure.outcome };\n state.onFakeEvent(\n failure.outcome === \"evicted\"\n ? {\n ...event,\n kind: \"evicted\",\n code: failure.code,\n message: failure.message,\n ...failure.evicted,\n }\n : { ...event, kind: failure.outcome, code: failure.code, message: failure.message },\n );\n return jsonRpcError(id, MCP_FAKE_ERROR_CODES[failure.code], failure.message, {\n aimock: failure.aimock,\n });\n };\n\n const dispatcher = createJsonRpcDispatcher({\n methods: {\n // initialize is handled directly in the outer function — this entry is\n // only here so the dispatcher doesn't return \"Method not found\" if the\n // request somehow reaches it.\n initialize: async (_params, id) => {\n return jsonRpcResult(id, {\n protocolVersion: \"2025-03-26\",\n capabilities: capabilities(),\n serverInfo: state.serverInfo,\n });\n },\n\n \"notifications/initialized\": async (_params, _id, req) => {\n const sessionId = req.headers[\"mcp-session-id\"] as string;\n const session = state.sessions.get(sessionId);\n if (session) {\n session.initialized = true;\n }\n return null;\n },\n\n ping: async (_params, id) => {\n return jsonRpcResult(id, {});\n },\n\n // C7: only a mount with recorded log lines serves logging/setLevel;\n // every other mount answers exactly as the dispatcher does for an\n // unknown method (src/jsonrpc.ts).\n \"logging/setLevel\": async (params, id, req) => {\n if (!state.fakes.hasRecordedLogs()) return jsonRpcError(id, -32601, \"Method not found\");\n const level = (params ?? {}) as { level?: unknown };\n if (logLevelRank(level.level) === -1) {\n return jsonRpcError(id, -32602, \"Invalid log level\");\n }\n const sessionId = req.headers[\"mcp-session-id\"];\n if (typeof sessionId === \"string\") logLevels.set(sessionId, String(level.level));\n return jsonRpcResult(id, {});\n },\n\n \"tools/list\": async (_params, id, req) => {\n const tools: MCPToolDefinition[] = [];\n for (const { def } of state.tools.values()) {\n tools.push(def);\n }\n const ctx = contexts.get(req);\n // AM7: a recorded list comes first, in recorded order (its definition\n // wins), then registered tools missing from it, then the other fake\n // tools.\n const recorded = ctx ? state.fakes.recordedList(ctx.identity) : null;\n if (ctx && recorded) {\n const listed: unknown[] = [...recorded];\n const names = new Set(recorded.map((t) => t.name));\n for (const def of tools) {\n if (!names.has(def.name)) {\n names.add(def.name);\n listed.push(def);\n }\n }\n for (const fake of state.fakes.listTools(ctx.identity)) {\n if (!names.has(fake.name)) {\n names.add(fake.name);\n listed.push(fake);\n }\n }\n return jsonRpcResult(id, { tools: listed });\n }\n // Spec 10: the union with the applicable fake tools; a registered\n // definition wins for metadata.\n if (ctx) {\n const registered = new Set(tools.map((t) => t.name));\n for (const fake of state.fakes.listTools(ctx.identity)) {\n if (!registered.has(fake.name)) tools.push(fake);\n }\n }\n return jsonRpcResult(id, { tools });\n },\n\n \"tools/call\": async (params, id, req) => {\n // One outcome per invocation, in order, even when nothing is claimed,\n // so each message of a batch is matched with its own.\n const ctx = contexts.get(req);\n // A single call the outer handler already claimed: its outcome is\n // already in `calls`, and its claim is not run again.\n const precomputed = ctx?.precomputed;\n if (ctx) ctx.precomputed = undefined;\n const isNotification = !precomputed && ctx?.notifications[ctx.calls.length] === true;\n if (!precomputed) ctx?.calls.push({ mcpFake: null, error: null });\n const outcome: CallOutcome = ctx?.calls[ctx.calls.length - 1] ?? {\n mcpFake: null,\n error: null,\n };\n // A notification (no `id` member) gets no answer (JSON-RPC 2.0) and\n // claims no fake entry, but a registered handler still runs for its\n // side effects, as on origin/main. A request whose id is null or of\n // another type is still served, with id null.\n const { name: rawName, arguments: args } = (params ?? {}) as {\n name?: unknown;\n arguments?: unknown;\n };\n if (!rawName) {\n return jsonRpcError(id, -32602, \"Missing tool name\");\n }\n if (typeof rawName !== \"string\") {\n const got = Array.isArray(rawName) ? \"array\" : typeof rawName;\n return jsonRpcError(\n id,\n -32602,\n build(msg`Invalid tool name: expected a string, got ${fixed(got)}`),\n );\n }\n const name = rawName;\n // Spec 10 steps 1-2: fakes, then the deny policy; never a handler\n // after a fake failure.\n if (precomputed?.kind === \"reply\") return precomputed.value;\n if (ctx && !isNotification && !precomputed) {\n const fake = callFake(ctx, outcome, name, args, id);\n if (fake) return fake;\n }\n const entry = state.tools.get(name);\n // RP2: every tools/call no fake applied to is logged with who answers it.\n if (ctx && !isNotification) {\n state.onUnfaked({\n identity: ctx.identity,\n tool: name,\n mount: ctx.mount,\n args,\n answeredBy: !entry\n ? \"unknown-tool\"\n : entry.handler && configToolHandlers.has(entry.handler)\n ? \"config\"\n : entry.handler\n ? \"handler\"\n : \"empty\",\n });\n }\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown tool: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n const content: MCPContent[] = Array.isArray(result)\n ? result\n : [{ type: \"text\", text: String(result) }];\n return jsonRpcResult(id, { content, isError: false });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcResult(id, {\n content: [{ type: \"text\", text: message }],\n isError: true,\n });\n }\n }\n // No handler — return empty content\n return jsonRpcResult(id, { content: [], isError: false });\n },\n\n \"resources/list\": async (_params, id) => {\n const resources: MCPResourceDefinition[] = [];\n for (const { def } of state.resources.values()) {\n resources.push(def);\n }\n return jsonRpcResult(id, { resources });\n },\n\n \"resources/read\": async (params, id) => {\n const { uri } = (params ?? {}) as { uri?: string };\n if (!uri) {\n return jsonRpcError(id, -32602, \"Missing resource URI\");\n }\n const entry = state.resources.get(uri);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);\n }\n return jsonRpcResult(id, {\n contents: [\n {\n uri,\n ...(entry.content?.text !== undefined && { text: entry.content.text }),\n ...(entry.content?.blob !== undefined && { blob: entry.content.blob }),\n ...(entry.content?.mimeType !== undefined && { mimeType: entry.content.mimeType }),\n },\n ],\n });\n },\n\n \"prompts/list\": async (_params, id) => {\n const prompts: MCPPromptDefinition[] = [];\n for (const { def } of state.prompts.values()) {\n prompts.push(def);\n }\n return jsonRpcResult(id, { prompts });\n },\n\n \"prompts/get\": async (params, id) => {\n const { name, arguments: args } = (params ?? {}) as { name?: string; arguments?: unknown };\n if (!name) {\n return jsonRpcError(id, -32602, \"Missing prompt name\");\n }\n const entry = state.prompts.get(name);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown prompt: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n return jsonRpcResult(id, result);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcError(id, -32603, `Prompt handler error: ${message}`);\n }\n }\n // No handler — return empty messages\n return jsonRpcResult(id, { messages: [] });\n },\n },\n });\n\n /** Answer a rejected identity (H4, H5) with HTTP 400, in the session-error shape. */\n const rejectIdentity = (\n res: http.ServerResponse,\n resolution: Extract<McpIdentityResolution, { ok: false }>,\n ): void => {\n res.writeHead(resolution.error.status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: resolution.error.message, code: resolution.error.code }));\n };\n\n /**\n * L2: report each identity value that fell back to the raw value (H1),\n * naming its source (header or query) and the value itself, and any query\n * name that did not decode. `fellBack` says only that some value of the\n * field fell back; the header is checked on its own, and the query by the\n * same resolver given the query alone, so the rules are the resolver's.\n */\n const reportFallback = (\n sessionId: string,\n req: http.IncomingMessage,\n resolution: Extract<McpIdentityResolution, { ok: true }>,\n ): void => {\n if (!state.hasFakes()) return;\n const { fellBack } = resolution;\n if (fellBack.testId || fellBack.context) {\n const queryOnly = resolveMcpIdentity({ headers: {}, url: req.url });\n for (const field of [\"testId\", \"context\"] as const) {\n if (!fellBack[field]) continue;\n const header = req.headers[IDENTITY_HEADERS[field]];\n const headerSent = typeof header === \"string\" && header !== \"\";\n if (headerSent && decodeMcpHeaderValue(header).fellBack) {\n state.onDecodeFallback(sessionId, {\n kind: \"value\",\n field,\n source: \"header\",\n raw: header,\n used: true,\n });\n }\n const fromQuery =\n queryOnly.ok && queryOnly.fellBack[field] ? queryOnly.identity[field] : null;\n if (fromQuery !== null) {\n state.onDecodeFallback(sessionId, {\n kind: \"value\",\n field,\n source: \"query\",\n raw: fromQuery,\n used: !headerSent,\n });\n }\n }\n }\n if (resolution.undecodedQueryNames) {\n state.onDecodeFallback(sessionId, { kind: \"names\", count: resolution.undecodedQueryNames });\n }\n };\n\n /**\n * Handle one MCP request. `mount` is the path the mount is served at (I9;\n * `/` for a standalone server). Resolves with what the request resolved,\n * for the mount's journal entry.\n */\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n mount = \"/\",\n record: MCPRequestRecord = newMCPRequestRecord(),\n ): Promise<MCPRequestRecord> => {\n /** T1: recorded `atMs` / `durationMs` are measured from here. */\n const arrivedAt = performance.now();\n // DELETE handler: session teardown\n if (req.method === \"DELETE\") {\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return record;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return record;\n }\n // Journal only: identity inputs are not checked on a session teardown.\n const resolution = resolveMcpIdentity(req, state.sessions.get(sessionId));\n if (resolution.ok) record.identity = resolution.identity;\n state.sessions.delete(sessionId);\n logLevels.delete(sessionId);\n state.onSessionClosed(sessionId);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ ok: true }));\n return record;\n }\n\n // Identity inputs (I1-I3, H4, H5): on every POST, a bad undeclared\n // override or a repeated field is rejected before the request is served.\n const headerSessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const knownSession = headerSessionId ? state.sessions.get(headerSessionId) : undefined;\n\n // Parse the body to determine method for session validation\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n const resolution = resolveMcpIdentity(req, knownSession);\n if (!resolution.ok) {\n rejectIdentity(res, resolution);\n return record;\n }\n record.identity = resolution.identity;\n if (headerSessionId && knownSession) reportFallback(headerSessionId, req, resolution);\n // Let the dispatcher handle parse errors\n await dispatcher(req, res, body);\n return record;\n }\n\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? (parsed as { method: unknown }).method\n : undefined;\n\n // Handle initialize directly to control response headers\n if (method === \"initialize\") {\n const id =\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? (parsed as { id: unknown }).id\n : null;\n\n // I3: only the values the initialize request itself supplies are bound.\n const resolution = resolveMcpIdentity(req);\n if (!resolution.ok) {\n rejectIdentity(res, resolution);\n return record;\n }\n record.identity = resolution.identity;\n const { identity, supplied } = resolution;\n\n const sessionId = randomUUID();\n state.sessions.set(sessionId, {\n id: sessionId,\n initialized: false,\n createdAt: Date.now(),\n ...(supplied.testId && identity.testId !== null ? { testId: identity.testId } : {}),\n ...(supplied.context && identity.context !== null ? { context: identity.context } : {}),\n ...(supplied.undeclared && identity.undeclared !== null\n ? { undeclared: identity.undeclared }\n : {}),\n });\n reportFallback(sessionId, req, resolution);\n\n const response = {\n jsonrpc: \"2.0\",\n id,\n result: {\n protocolVersion: \"2025-03-26\",\n capabilities: capabilities(),\n serverInfo: state.serverInfo,\n },\n };\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Mcp-Session-Id\": sessionId,\n });\n res.end(JSON.stringify(response));\n return record;\n }\n\n const resolution = resolveMcpIdentity(req, knownSession);\n if (!resolution.ok) {\n rejectIdentity(res, resolution);\n return record;\n }\n record.identity = resolution.identity;\n\n // Session validation for all other methods\n const sessionId = headerSessionId;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return record;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return record;\n }\n reportFallback(sessionId, req, resolution);\n\n // Enforce initialization: only allow notifications/initialized through\n // before the session is fully initialized\n const session = state.sessions.get(sessionId)!;\n if (!session.initialized && method !== \"notifications/initialized\") {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify(\n jsonRpcError(\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? ((parsed as { id: unknown }).id as string | number)\n : null,\n -32002,\n \"Session not initialized\",\n ),\n ),\n );\n return record;\n }\n\n // Delegate to the JSON-RPC dispatcher for all other methods\n // The dispatcher invokes `tools/call` once per message that is an object\n // with `jsonrpc: \"2.0\"` and that method, in order (see B2 below).\n const notifications = (Array.isArray(parsed) ? parsed : [parsed])\n .filter((m): m is Record<string, unknown> => isToolsCall(m) && m.jsonrpc === \"2.0\")\n .map((m) => !(\"id\" in m));\n const ctx: FakeRequestContext = {\n identity: resolution.identity,\n mount,\n notifications,\n calls: [],\n };\n contexts.set(req, ctx);\n // MR15 / T1: a single tools/call is claimed here, before the dispatcher,\n // because an answer with recorded timing or notifications is written by\n // `writeFakeToolAnswer` (SSE or delayed JSON), which the dispatcher's\n // JSON-only writer cannot do. Any other outcome is handed to the method\n // through `ctx.precomputed`, so the claim runs once.\n const single = singleToolsCall(parsed);\n if (single) {\n const outcome: CallOutcome = { mcpFake: null, error: null };\n ctx.calls.push(outcome);\n const fake = callFake(ctx, outcome, single.name, single.args, single.id);\n const replay =\n fake && outcome.mcpFake?.outcome === \"answered\" && outcome.mcpFake.id !== null\n ? state.fakes.replayOf(outcome.mcpFake.id)\n : null;\n if (\n fake &&\n \"result\" in fake &&\n replay &&\n (replay.notifications.length > 0 ||\n (replay.timing === \"recorded\" && replay.durationMs !== undefined))\n ) {\n await writeFakeToolAnswer(\n req,\n res,\n single.id,\n fake.result,\n replay,\n single.params,\n state.replaySpeed(),\n logLevels.get(sessionId) ?? null,\n arrivedAt,\n );\n record.messages = [\n { body: single.message, mcpFake: outcome.mcpFake, error: outcome.error },\n ];\n return record;\n }\n ctx.precomputed = fake ? { kind: \"reply\", value: fake } : { kind: \"none\" };\n }\n await dispatcher(req, res, body);\n // B2: each tools/call message is journaled with its body and its own\n // outcome. The dispatcher invokes `tools/call` once per message that is\n // an object with `jsonrpc: \"2.0\"` and that method, in order, so the n-th\n // such message takes the n-th outcome.\n let next = 0;\n const messageOf = (message: unknown): MCPMessageRecord => {\n if (!isToolsCall(message)) return { body: null, mcpFake: null, error: null };\n const outcome = message.jsonrpc === \"2.0\" ? ctx.calls[next++] : undefined;\n return { body: message, mcpFake: outcome?.mcpFake ?? null, error: outcome?.error ?? null };\n };\n record.messages = Array.isArray(parsed) ? parsed.map(messageOf) : [messageOf(parsed)];\n return record;\n };\n}\n\n/**\n * A single JSON-RPC `tools/call` request (not a batch, not a notification)\n * with a string or number id and a string tool name, else `null`. Only such\n * a message is claimed by the outer handler (MR15).\n */\nfunction singleToolsCall(parsed: unknown): {\n message: Record<string, unknown>;\n id: string | number;\n name: string;\n args: unknown;\n params: unknown;\n} | null {\n if (!isToolsCall(parsed) || parsed.jsonrpc !== \"2.0\" || !(\"id\" in parsed)) return null;\n const id = parsed.id;\n if (typeof id !== \"string\" && typeof id !== \"number\") return null;\n const params = parsed.params;\n if (typeof params !== \"object\" || params === null) return null;\n const name: unknown = (params as { name?: unknown }).name;\n if (typeof name !== \"string\" || name === \"\") return null;\n return { message: parsed, id, name, args: (params as { arguments?: unknown }).arguments, params };\n}\n\n/** Resolve after `ms` (no wait when `ms <= 0`). */\nfunction sleep(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\n/** The client's `_meta.progressToken` of a request's params, else `undefined`. */\nfunction progressTokenOf(params: unknown): string | number | undefined {\n if (typeof params !== \"object\" || params === null) return undefined;\n const meta: unknown = (params as { _meta?: unknown })._meta;\n if (typeof meta !== \"object\" || meta === null) return undefined;\n const token: unknown = (meta as { progressToken?: unknown }).progressToken;\n return typeof token === \"string\" || typeof token === \"number\" ? token : undefined;\n}\n\n/**\n * FA3 gate of one recorded notification for this request: the params to send,\n * or `null` when it does not replay. `notifications/progress` replays only\n * when the request carries a progress token, which replaces the recorded one;\n * `notifications/message` only when the session set a log level (C7) and the\n * line is at or above it. No other method replays.\n */\nfunction replayedParams(\n notification: McpFakeNotification,\n progressToken: string | number | undefined,\n logLevel: string | null,\n): Record<string, unknown> | null {\n if (notification.method === \"notifications/progress\") {\n return progressToken === undefined ? null : { ...notification.params, progressToken };\n }\n if (notification.method === \"notifications/message\") {\n const min = logLevelRank(logLevel);\n const rank = logLevelRank(notification.params.level);\n return min !== -1 && rank >= min ? { ...notification.params } : null;\n }\n return null;\n}\n\n/**\n * Write a fake's tools/call answer. SSE iff the entry has a notification whose\n * FA3 gate passes AND Accept lists text/event-stream; else JSON. Timing per T1\n * (value / speed) unless the block's timing is \"immediate\". `logLevel` is the\n * session's `logging/setLevel` level (C7), `arrivedAt` the `performance.now()`\n * of the request's arrival, which `atMs` and `durationMs` are measured from.\n */\nexport async function writeFakeToolAnswer(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string | number | null,\n result: unknown,\n replay: ReturnType<McpFakeStore[\"replayOf\"]>,\n params: unknown,\n speed: number,\n logLevel: string | null = null,\n arrivedAt: number = performance.now(),\n): Promise<void> {\n const timed = replay !== null && replay.timing === \"recorded\";\n const waitUntil = (atMs: number): Promise<void> =>\n timed ? sleep(atMs / speed - (performance.now() - arrivedAt)) : Promise.resolve();\n const token = progressTokenOf(params);\n const notifications = [...(replay?.notifications ?? [])]\n .sort((a, b) => a.atMs - b.atMs)\n .flatMap((n) => {\n const sent = replayedParams(n, token, logLevel);\n return sent ? [{ atMs: n.atMs, method: n.method, params: sent }] : [];\n });\n const accept = req.headers.accept ?? \"\";\n const answer = { jsonrpc: \"2.0\" as const, id, result };\n if (notifications.length > 0 && accept.includes(\"text/event-stream\")) {\n res.writeHead(200, { \"Content-Type\": \"text/event-stream\", \"Cache-Control\": \"no-cache\" });\n res.flushHeaders();\n for (const n of notifications) {\n await waitUntil(n.atMs);\n if (res.destroyed) return;\n res.write(\n `event: message\\ndata: ${JSON.stringify({ jsonrpc: \"2.0\", method: n.method, params: n.params })}\\n\\n`,\n );\n }\n if (replay?.durationMs !== undefined) await waitUntil(replay.durationMs);\n if (res.destroyed) return;\n res.end(`event: message\\ndata: ${JSON.stringify(answer)}\\n\\n`);\n return;\n }\n if (replay?.durationMs !== undefined) await waitUntil(replay.durationMs);\n if (res.destroyed) return;\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(answer));\n}\n\n/** The identity header of each field whose value can fall back (H1). */\nconst IDENTITY_HEADERS: Record<McpDecodeField, string> = {\n testId: \"x-test-id\",\n context: \"x-aimock-context\",\n};\n\n/** A JSON-RPC message object whose method is `tools/call`. */\nfunction isToolsCall(message: unknown): message is Record<string, unknown> {\n return (\n typeof message === \"object\" &&\n message !== null &&\n !Array.isArray(message) &&\n \"method\" in message &&\n message.method === \"tools/call\"\n );\n}\n"],"mappings":"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"}
package/dist/mcp-mock.cjs CHANGED
@@ -1,6 +1,10 @@
1
1
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_message_text = require('./message-text.cjs');
2
3
  const require_helpers = require('./helpers.cjs');
4
+ const require_record_sanitize = require('./record-sanitize.cjs');
3
5
  const require_mcp_handler = require('./mcp-handler.cjs');
6
+ const require_mcp_fakes = require('./mcp-fakes.cjs');
7
+ const require_mcp_recorder = require('./mcp-recorder.cjs');
4
8
  let node_http = require("node:http");
5
9
  node_http = require_runtime.__toESM(node_http);
6
10
 
@@ -10,9 +14,25 @@ var MCPMock = class {
10
14
  resources = /* @__PURE__ */ new Map();
11
15
  prompts = /* @__PURE__ */ new Map();
12
16
  sessions = /* @__PURE__ */ new Map();
17
+ /** This mount's fakes; kept across `reset()` so the id counters are never rewound. */
18
+ fakes = new require_mcp_fakes.McpFakeStore();
19
+ /** Fake blocks loaded since the last clear. */
20
+ fakeBlocks = 0;
21
+ /**
22
+ * Decode fallbacks already reported (L2), per live session: one key per
23
+ * field and source, plus one for undecodable query names. Dropped when the
24
+ * session is deleted.
25
+ */
26
+ decodeWarned = /* @__PURE__ */ new Map();
13
27
  server = null;
14
28
  journal = null;
15
29
  registry = null;
30
+ /** Mount logger for L1, L2 and L10; none (lines dropped) until `setLogger`. */
31
+ logger = null;
32
+ /** T1: recorded timing (`durationMs`, `atMs`) plays at value / speed. */
33
+ replaySpeed = 1;
34
+ /** MR1: the recorder while recording is on, else null (AM1). */
35
+ recorder = null;
16
36
  options;
17
37
  requestHandler;
18
38
  constructor(options) {
@@ -46,10 +66,140 @@ var MCPMock = class {
46
66
  });
47
67
  return this;
48
68
  }
69
+ /**
70
+ * Load MCP fakes from code: one `mcpFakes` value (a block, or an array of
71
+ * blocks), in the fixture-file shape, validated here. One run-time addition
72
+ * (`code#<n>` entry ids). Always appends. Returns the shadowed-entry
73
+ * warnings; on any bad block throws one `McpFakesAddError` and adds nothing.
74
+ */
75
+ loadFakes(blocks) {
76
+ const sources = Array.isArray(blocks) ? blocks.map((raw, blockIndex) => ({
77
+ source: "code",
78
+ blockIndex,
79
+ raw
80
+ })) : [{
81
+ source: "code",
82
+ blockIndex: null,
83
+ raw: blocks
84
+ }];
85
+ return this.addMcpFakes(sources, { kind: "code" });
86
+ }
87
+ addMcpFakes(blocks, origin) {
88
+ return this.addFakes(blocks, origin);
89
+ }
90
+ /** `addMcpFakes`, plus the internal `record` origin of the MCP recorder. */
91
+ addFakes(blocks, origin) {
92
+ const result = this.fakes.add(blocks, origin);
93
+ this.fakeBlocks += blocks.length;
94
+ return result;
95
+ }
96
+ /**
97
+ * The blocks on this mount that apply to `testId` and `context` (spec 6.5,
98
+ * I9), with every entry id and its consumed state for that test id. `null`
99
+ * means "none sent". Read-only: the result is deeply frozen.
100
+ */
101
+ fakesSnapshot(testId = null, context = null) {
102
+ return this.fakes.snapshot(testId, context);
103
+ }
104
+ clearMcpFakes() {
105
+ this.fakes.clear();
106
+ this.fakeBlocks = 0;
107
+ this.recorder?.reset();
108
+ }
109
+ resetScenarioState(testId) {
110
+ this.fakes.resetState(testId);
111
+ this.recorder?.reset(testId);
112
+ }
113
+ /**
114
+ * Record a real upstream MCP server (MR1): from now on every request to
115
+ * this mount is forwarded to `config.upstream` (all paths and methods), a
116
+ * fake that applies still answers locally, and each forwarded `tools/list`
117
+ * and `tools/call` is written to an `mcpFakes` file under
118
+ * `config.fixturePath`. `secretValues` and `upstreamAuth` that are
119
+ * `undefined` come from `AIMOCK_RECORD_SECRET_VALUES` and
120
+ * `AIMOCK_MCP_UPSTREAM_AUTH`, as on the CLI and config paths. Throws on an
121
+ * invalid upstream URL, a malformed `upstreamAuth` (or env auth), or a
122
+ * `secretValues` entry shorter than 8 characters.
123
+ */
124
+ enableRecording(config) {
125
+ let resolved = config;
126
+ if (config.secretValues === void 0 || config.upstreamAuth === void 0) {
127
+ const fromEnv = require_mcp_recorder.mcpRecordEnv(process.env);
128
+ resolved = {
129
+ ...config,
130
+ secretValues: config.secretValues ?? fromEnv.secretValues,
131
+ upstreamAuth: config.upstreamAuth ?? fromEnv.upstreamAuth
132
+ };
133
+ }
134
+ require_record_sanitize.validateSecretValues(resolved.secretValues ?? []);
135
+ const recorder = new require_mcp_recorder.McpRecorder(this.recorderHost(), resolved);
136
+ this.recorder?.close();
137
+ this.recorder = recorder;
138
+ return this;
139
+ }
140
+ /** Stop recording: the mount serves exactly as before `enableRecording`. */
141
+ disableRecording() {
142
+ this.recorder?.close();
143
+ this.recorder = null;
144
+ return this;
145
+ }
146
+ /** @internal MR13: the recorder's write hashes and write listener, or null when not recording. */
147
+ recorderEvents() {
148
+ return this.recorder;
149
+ }
150
+ setLogger(logger) {
151
+ this.logger = logger;
152
+ }
153
+ /** T1: the speed recorded fake timing plays at; a non-positive or non-finite value is ignored. */
154
+ setReplaySpeed(speed) {
155
+ if (Number.isFinite(speed) && speed > 0) this.replaySpeed = speed;
156
+ }
157
+ /** @internal RP2 report part for this mount (src/mcp-fakes-report.ts reads it). */
158
+ fakesReportPart(testId, context, mount) {
159
+ return this.fakes.reportPart(testId, context, mount);
160
+ }
49
161
  async handleRequest(req, res, pathname) {
162
+ if (this.recorder) {
163
+ await this.recorder.handle(req, res, mountPathOf(req.url, pathname), pathname);
164
+ return true;
165
+ }
50
166
  if (pathname !== "/" && pathname !== "") return false;
51
- if (req.method !== "POST" && req.method !== "DELETE") return false;
52
- const body = await require_helpers.readBody(req);
167
+ if (req.method === "GET") {
168
+ this.answerMethodNotAllowed(req, res);
169
+ return true;
170
+ }
171
+ if (!isServedMethod(req.method)) return false;
172
+ await this.serve(req, res, mountPathOf(req.url, pathname), false);
173
+ return true;
174
+ }
175
+ /**
176
+ * Serve one request, in both modes (mounted: a POST or DELETE on the mount
177
+ * root; standalone: any method but GET on any path): count it, run the
178
+ * handler, journal it. A throw from the mount's own code is logged with the
179
+ * identity resolved so far, answered 500 and journaled with its message,
180
+ * never left to a generic catch.
181
+ */
182
+ async serve(req, res, mount, standalone) {
183
+ const record = require_mcp_handler.newMCPRequestRecord();
184
+ try {
185
+ const body = await require_helpers.readBody(req);
186
+ this.countRequest(req, body);
187
+ await this.requestHandler(req, res, body, mount, record);
188
+ this.journalRequest(req, res, record);
189
+ } catch (err) {
190
+ const thrown = err instanceof Error ? err.message : String(err);
191
+ const line = require_message_text.build(require_message_text.msg`MCPMock request error (${record.identity ? require_mcp_handler.describeMcpIdentity(record.identity) : require_message_text.msg`no identity resolved`}, mount ${require_message_text.plainText(mount)}): ${require_message_text.plainText(thrown)}`);
192
+ if (standalone || !this.logger) console.error(line);
193
+ else this.logger.error(line);
194
+ if (!res.headersSent) {
195
+ res.writeHead(500);
196
+ res.end("Internal server error");
197
+ } else if (!res.writableEnded) res.end();
198
+ this.journalRequest(req, res, record, thrown);
199
+ }
200
+ }
201
+ /** `aimock_mcp_requests_total` by JSON-RPC method (`session/delete` for DELETE). */
202
+ countRequest(req, body) {
53
203
  if (this.registry) if (req.method === "DELETE") this.registry.incrementCounter("aimock_mcp_requests_total", { method: "session/delete" });
54
204
  else try {
55
205
  const parsed = JSON.parse(body);
@@ -58,19 +208,19 @@ var MCPMock = class {
58
208
  } catch {
59
209
  this.registry.incrementCounter("aimock_mcp_requests_total", { method: "unknown" });
60
210
  }
61
- await this.requestHandler(req, res, body);
62
- if (this.journal) this.journal.add({
63
- method: req.method ?? "POST",
64
- path: req.url ?? "/",
65
- headers: require_helpers.flattenHeaders(req.headers),
66
- body: null,
67
- service: "mcp",
68
- response: {
69
- status: res.statusCode,
70
- fixture: null
71
- }
211
+ }
212
+ /**
213
+ * B1: answer 405 (`Allow: POST, DELETE`), journaled. Mounted, this is a GET
214
+ * on the mount root; standalone, a GET on any path.
215
+ */
216
+ answerMethodNotAllowed(req, res) {
217
+ res.writeHead(405, {
218
+ "Content-Type": "application/json",
219
+ Allow: "POST, DELETE"
72
220
  });
73
- return true;
221
+ res.end(JSON.stringify({ error: require_message_text.build(require_message_text.msg`Method not allowed: use POST or DELETE`) }));
222
+ req.resume();
223
+ this.journalRequest(req, res, null);
74
224
  }
75
225
  health() {
76
226
  return {
@@ -83,6 +233,7 @@ var MCPMock = class {
83
233
  }
84
234
  setJournal(journal) {
85
235
  this.journal = journal;
236
+ this.fakes.setMaxTestIds(journal.fixtureCountsMaxTestIdsCap);
86
237
  }
87
238
  setRegistry(registry) {
88
239
  this.registry = registry;
@@ -93,40 +244,20 @@ var MCPMock = class {
93
244
  const port = this.options.port ?? 0;
94
245
  return new Promise((resolve, reject) => {
95
246
  const srv = node_http.createServer((req, res) => {
96
- const chunks = [];
97
- req.on("data", (chunk) => chunks.push(chunk));
98
- req.on("end", () => {
99
- const body = Buffer.concat(chunks).toString();
100
- this.requestHandler(req, res, body).then(() => {
101
- if (this.journal) this.journal.add({
102
- method: req.method ?? "POST",
103
- path: req.url ?? "/",
104
- headers: require_helpers.flattenHeaders(req.headers),
105
- body: null,
106
- service: "mcp",
107
- response: {
108
- status: res.statusCode,
109
- fixture: null
110
- }
111
- });
112
- }).catch((err) => {
247
+ if (this.recorder) {
248
+ this.recorder.handle(req, res, "/", "/").catch((err) => {
113
249
  console.error("MCPMock request error:", err);
114
- if (!res.headersSent) {
115
- res.writeHead(500);
116
- res.end("Internal server error");
117
- } else if (!res.writableEnded) res.end();
118
- if (this.journal) this.journal.add({
119
- method: req.method ?? "POST",
120
- path: req.url ?? "/",
121
- headers: require_helpers.flattenHeaders(req.headers),
122
- body: null,
123
- service: "mcp",
124
- response: {
125
- status: res.statusCode,
126
- fixture: null
127
- }
128
- });
250
+ if (!res.writableEnded) res.end();
129
251
  });
252
+ return;
253
+ }
254
+ if (req.method === "GET") {
255
+ this.answerMethodNotAllowed(req, res);
256
+ return;
257
+ }
258
+ this.serve(req, res, "/", true).catch((err) => {
259
+ console.error("MCPMock request error:", err);
260
+ if (!res.writableEnded) res.end();
130
261
  });
131
262
  });
132
263
  srv.listen(port, host, () => {
@@ -158,9 +289,106 @@ var MCPMock = class {
158
289
  this.resources.clear();
159
290
  this.prompts.clear();
160
291
  this.sessions.clear();
292
+ this.decodeWarned.clear();
293
+ this.clearMcpFakes();
161
294
  this.requestHandler = this.buildHandler();
162
295
  return this;
163
296
  }
297
+ /**
298
+ * The journal entries of one handled request (B2, B3): one per JSON-RPC
299
+ * message (each element of a batch), or one with no body when the request
300
+ * carried none that was read; none without a journal (W5). `error` is what
301
+ * the mount's own code threw while serving it.
302
+ */
303
+ journalRequest(req, res, record, error) {
304
+ if (!this.journal) return;
305
+ const messages = record?.messages.length ? record.messages : [{
306
+ body: null,
307
+ mcpFake: null,
308
+ error: null
309
+ }];
310
+ for (const message of messages) {
311
+ const failed = message.error ?? error;
312
+ this.journal.add({
313
+ method: req.method ?? "POST",
314
+ path: req.url ?? "/",
315
+ headers: require_helpers.flattenHeaders(req.headers),
316
+ body: message.body,
317
+ service: "mcp",
318
+ ...record?.identity ? {
319
+ testId: record.identity.testId ?? require_mcp_fakes.MCP_FAKES_DEFAULT_TEST_ID,
320
+ context: record.identity.context
321
+ } : {},
322
+ response: {
323
+ status: res.statusCode,
324
+ fixture: null,
325
+ ...message.mcpFake ? { mcpFake: message.mcpFake } : {},
326
+ ...failed !== void 0 && failed !== null ? { error: failed } : {}
327
+ }
328
+ });
329
+ }
330
+ }
331
+ /**
332
+ * Side channels of a fake outcome (9.3): the failure metric and the L1 /
333
+ * L10 line. A claim that threw (`internal_error`) is counted and logged
334
+ * like a failure, under `MCP_FAKE_INTERNAL_ERROR`.
335
+ */
336
+ onFakeEvent(evt) {
337
+ this.fakes.logEvent(evt.identity, toReportEvent(evt));
338
+ if (evt.kind === "answered") return;
339
+ this.registry?.incrementCounter("aimock_mcp_fake_failures_total", { code: evt.code });
340
+ if (!this.logger) return;
341
+ const where = require_message_text.msg`mount ${require_message_text.plainText(evt.mount)}`;
342
+ const kind = evt.kind.replace("_", " ");
343
+ const line = evt.kind === "evicted" ? require_message_text.msg`MCP-FAKE: evicted testId ${require_message_text.quote(evt.testId)} (cap ${evt.cap}) for tools/call ${require_message_text.plainText(evt.tool)} (${where})` : require_message_text.msg`MCP-FAKE: ${require_message_text.fixed(kind)} for tools/call ${require_message_text.plainText(evt.tool)} (${require_mcp_handler.describeMcpIdentity(evt.identity)}, ${where}): ${require_message_text.plainText(evt.message)}`;
344
+ this.logger.error(require_message_text.build(line));
345
+ }
346
+ /** RP2, C9: a tools/call no fake applied to is always logged, with who answered it. */
347
+ logUnfaked(evt) {
348
+ this.fakes.logEvent(evt.identity, {
349
+ outcome: "unfaked",
350
+ answeredBy: evt.answeredBy,
351
+ tool: evt.tool,
352
+ args: evt.args,
353
+ context: evt.identity.context,
354
+ mount: evt.mount
355
+ });
356
+ }
357
+ /**
358
+ * L2: one warning per session for each identity field and source whose
359
+ * value is not valid percent-encoding (H1), naming that source and value,
360
+ * and one for query names that are not valid percent-encoding.
361
+ */
362
+ onDecodeFallback(sessionId, fallback) {
363
+ let warned = this.decodeWarned.get(sessionId);
364
+ if (!warned) {
365
+ warned = /* @__PURE__ */ new Set();
366
+ this.decodeWarned.set(sessionId, warned);
367
+ }
368
+ const key = fallback.kind === "names" ? "names" : `${fallback.field}:${fallback.source}`;
369
+ if (warned.has(key)) return;
370
+ warned.add(key);
371
+ const session = require_message_text.quote(sessionId);
372
+ if (fallback.kind === "names") {
373
+ const one = fallback.count === 1;
374
+ this.logger?.warn(require_message_text.build(require_message_text.msg`MCP-FAKE: ${fallback.count} query parameter ${require_message_text.fixed(one ? "name" : "names")} on session ${session} ${require_message_text.fixed(one ? "is" : "are")} not valid percent-encoding; ${require_message_text.fixed(one ? "it matches" : "they match")} no identity field`));
375
+ return;
376
+ }
377
+ const label = DECODE_LABELS[fallback.field][fallback.source];
378
+ const fate = fallback.used ? "it is used as sent" : "it is ignored (the header value is used)";
379
+ this.logger?.warn(require_message_text.build(require_message_text.msg`MCP-FAKE: ${require_message_text.fixed(label)} value ${require_message_text.quote(fallback.raw)} on session ${session} is not valid percent-encoding; ${require_message_text.fixed(fate)}`));
380
+ }
381
+ /** The recorder's view of this mount (fakes, journal, logger, replay speed, side channels). */
382
+ recorderHost() {
383
+ return {
384
+ fakes: this.fakes,
385
+ addMcpFakes: (blocks, origin) => this.addFakes(blocks, origin),
386
+ journalEntry: (entry) => void this.journal?.add(entry),
387
+ logger: () => this.logger,
388
+ replaySpeed: () => this.replaySpeed,
389
+ onFakeEvent: (evt) => this.onFakeEvent(evt)
390
+ };
391
+ }
164
392
  buildHandler() {
165
393
  return require_mcp_handler.createMCPRequestHandler({
166
394
  serverInfo: this.options.serverInfo ?? {
@@ -170,10 +398,61 @@ var MCPMock = class {
170
398
  tools: this.tools,
171
399
  resources: this.resources,
172
400
  prompts: this.prompts,
173
- sessions: this.sessions
401
+ sessions: this.sessions,
402
+ fakes: this.fakes,
403
+ hasFakes: () => this.fakeBlocks > 0,
404
+ onFakeEvent: (evt) => this.onFakeEvent(evt),
405
+ onDecodeFallback: (sessionId, fallback) => this.onDecodeFallback(sessionId, fallback),
406
+ onSessionClosed: (sessionId) => void this.decodeWarned.delete(sessionId),
407
+ onUnfaked: (evt) => this.logUnfaked(evt),
408
+ replaySpeed: () => this.replaySpeed
174
409
  });
175
410
  }
176
411
  };
412
+ /** RP2: a fake outcome as an event-log entry (the store assigns `seq`). */
413
+ function toReportEvent(evt) {
414
+ const base = {
415
+ tool: evt.tool,
416
+ args: evt.args,
417
+ context: evt.identity.context,
418
+ mount: evt.mount
419
+ };
420
+ return evt.kind === "answered" ? {
421
+ ...base,
422
+ outcome: "answered",
423
+ entryId: evt.entryId
424
+ } : {
425
+ ...base,
426
+ outcome: evt.kind,
427
+ code: evt.code
428
+ };
429
+ }
430
+ /** B1: the only methods the mounted mount root serves; every other one is 405. */
431
+ function isServedMethod(method) {
432
+ return method === "POST" || method === "DELETE";
433
+ }
434
+ /** The L2 label of each identity field's header and query source. */
435
+ const DECODE_LABELS = {
436
+ testId: {
437
+ header: "X-Test-Id header",
438
+ query: "?testId= query parameter"
439
+ },
440
+ context: {
441
+ header: "X-AIMock-Context header",
442
+ query: "?context= query parameter"
443
+ }
444
+ };
445
+ /**
446
+ * The path a mount is served at (I9): the request path without the
447
+ * sub-path the server handed the mount (`/mcp` for `/mcp` or `/mcp/`), `/`
448
+ * at the root.
449
+ */
450
+ function mountPathOf(url, subPath) {
451
+ const full = new URL(url ?? "/", "http://mount.invalid").pathname;
452
+ const sub = subPath === "" ? "/" : subPath;
453
+ const mount = full.endsWith(sub) ? full.slice(0, full.length - sub.length) : full;
454
+ return mount === "" ? "/" : mount;
455
+ }
177
456
 
178
457
  //#endregion
179
458
  exports.MCPMock = MCPMock;