@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * MCP emulator state, seed, profiles, and minted value forms (internal).
3
+ *
4
+ * The state holds the modern listing the seed selects and the legacy sessions minted since the
5
+ * last reset or seed; a seed holds no session, so no seeded value can take the minted
6
+ * `yolk-emu-session-<n>` form. The session counter and the cursor generation are runtime data
7
+ * that never reset (`McpRuntime`).
8
+ *
9
+ * @experimental
10
+ */
11
+ import { createHash } from 'node:crypto'
12
+ import { Result } from 'effect'
13
+ import * as Schema from 'effect/Schema'
14
+
15
+ /** The reserved origin both synthetic profiles answer on; another origin is not emulated. */
16
+ export const mcpEmulatorOrigin = 'https://mcp.example.test'
17
+
18
+ /** The synthetic modern server: stateless `2026-07-28`, JSON answers. */
19
+ export const mcpEmulatorModernPath = '/modern/mcp'
20
+
21
+ /** The synthetic legacy server: an `initialize` handshake, a session, and SSE answers. */
22
+ export const mcpEmulatorLegacyPath = '/legacy/mcp'
23
+
24
+ /**
25
+ * The public reserved invalid credential of the MCP auth case: the only credential value the
26
+ * emulator compares a bearer with (through its digest), answered the recorded 401.
27
+ */
28
+ export const mcpEmulatorReservedInvalidCredential = 'yolk-conformance-invalid-credential-0000'
29
+
30
+ /** The minted session id form (`yolk-emu-session-<n>`, `n` from a counter that never resets). */
31
+ export const mcpEmulatorSessionPrefix = 'yolk-emu-session-'
32
+
33
+ /** At most this many sessions are held; an `initialize` beyond it is refused before any fault. */
34
+ export const mcpEmulatorSessionCap = 256
35
+
36
+ /**
37
+ * Which recorded modern listing `tools/list` answers: `one-page` (the listing every calling
38
+ * fixture records; the default) or `two-pages` (the paged listing of `mcp.tools.list`, whose first
39
+ * page issues a cursor). Both come from fixtures; neither is synthesised.
40
+ */
41
+ export const McpEmulatorModernListing = Schema.Literals(['one-page', 'two-pages'])
42
+
43
+ export type McpEmulatorModernListing = typeof McpEmulatorModernListing.Type
44
+
45
+ /** A legacy session: minted by `initialize`, ready after `notifications/initialized`. */
46
+ export const McpEmulatorSession = Schema.Struct({
47
+ id: Schema.String,
48
+ phase: Schema.Literals(['initializing', 'ready'])
49
+ })
50
+
51
+ export type McpEmulatorSession = typeof McpEmulatorSession.Type
52
+
53
+ /** The emulator state (a JSON value the core snapshots and restores). */
54
+ export const McpEmulatorStateSchema = Schema.Struct({
55
+ modernListing: McpEmulatorModernListing,
56
+ sessions: Schema.Array(McpEmulatorSession)
57
+ })
58
+
59
+ export type McpEmulatorState = {
60
+ modernListing: McpEmulatorModernListing
61
+ sessions: ReadonlyArray<McpEmulatorSession>
62
+ }
63
+
64
+ /** The typed seed: only the modern listing; it holds no session. */
65
+ export const McpEmulatorSeed = Schema.Struct({
66
+ modernListing: Schema.optionalKey(McpEmulatorModernListing)
67
+ })
68
+
69
+ export type McpEmulatorSeed = typeof McpEmulatorSeed.Type
70
+
71
+ const strict = { onExcessProperty: 'error' } as const
72
+
73
+ const decodeSeed = Schema.decodeUnknownResult(McpEmulatorSeed, strict)
74
+
75
+ const decodeStateSchema = Schema.decodeUnknownResult(McpEmulatorStateSchema, strict)
76
+
77
+ const issueMessage = (issue: Schema.SchemaError['issue']): string =>
78
+ new Schema.SchemaError(issue).message
79
+
80
+ /** Build the state of a seed (no sessions), or why the seed is invalid. */
81
+ export const buildSeedState = (input: unknown): McpEmulatorState | string => {
82
+ const decoded = decodeSeed(input)
83
+
84
+ if (Result.isFailure(decoded)) return issueMessage(decoded.failure.issue)
85
+
86
+ return { modernListing: decoded.success.modernListing ?? 'one-page', sessions: [] }
87
+ }
88
+
89
+ /** Decode a whole state (the core's seed validation), or why it is invalid. */
90
+ export const decodeState = (input: unknown): McpEmulatorState | string => {
91
+ const decoded = decodeStateSchema(input)
92
+
93
+ return Result.isFailure(decoded)
94
+ ? issueMessage(decoded.failure.issue)
95
+ : { modernListing: decoded.success.modernListing, sessions: decoded.success.sessions }
96
+ }
97
+
98
+ /**
99
+ * The one-way digest routes see instead of the bearer: SHA-256 of the arrival origin, a space,
100
+ * and the bearer (the wrapper's opt-in `bearerDigest`).
101
+ */
102
+ export const mcpBearerDigest = (bearer: string, origin: string): string =>
103
+ createHash('sha256').update(`${origin} ${bearer}`, 'utf8').digest('hex')
104
+
105
+ /**
106
+ * Runtime data (never in the state): the session counter and the cursor generation never reset;
107
+ * the cursor issued in the current generation is cleared on every reset and seed.
108
+ */
109
+ export type McpRuntime = {
110
+ /** The number of the next minted session (never reset). */
111
+ nextSession: number
112
+ /** Starts at 1; every reset and seed starts the next generation (never reset). */
113
+ generation: number
114
+ /** The generation that first issued the recorded cursor (never reset). */
115
+ cursorFirstGeneration: number | undefined
116
+ /** The cursor issued in the current generation, if any. */
117
+ issuedCursor: string | undefined
118
+ }
119
+
120
+ export const makeMcpRuntime = (): McpRuntime => ({
121
+ nextSession: 1,
122
+ generation: 1,
123
+ cursorFirstGeneration: undefined,
124
+ issuedCursor: undefined
125
+ })
package/src/mcp.ts ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Stateful MCP emulator (the two synthetic servers of the MCP conformance fixtures: profile
3
+ * `synthetic-modern` on `https://mcp.example.test/modern/mcp`, stateless `2026-07-28` with JSON
4
+ * answers, and profile `synthetic-legacy` on `https://mcp.example.test/legacy/mcp`, an
5
+ * `initialize` handshake with sessions and SSE answers), built on the upstream `@emulators/core`
6
+ * custom runtime, with a request ledger, status and truncation faults, and an `/_emulate/*`
7
+ * control plane.
8
+ *
9
+ * It never imports SDK code: its answers are the sixteen synthetic `@yolk-sdk/mcp/conformance`
10
+ * fixtures copied as data (`src/mcp/recordings.ts`), and every manifest row names the conformance
11
+ * cases whose fixtures it answers (`mcpEmulatorRoutes`: one `RPC <origin><path>#<method>` row per
12
+ * recorded JSON-RPC method of each profile, plus the legacy `GET` row; no row writes anything in a
13
+ * real service). Response behaviour comes only from the fixtures, byte for byte: a JSON-RPC POST is
14
+ * answered only when it equals a recorded request within the latitude below, with the recorded
15
+ * answer, the request id substituted at exactly the recorded place (the top-level `id` of a JSON
16
+ * answer, or the `id` of the SSE response event's payload; notification events and SSE `id:` lines
17
+ * stay byte for byte; an answer that does not carry the recorded request id, such as the legacy era
18
+ * probe's `id: null` error, is unchanged). The legacy `initialize` mints a session
19
+ * `yolk-emu-session-<n>` (`n` from a counter that never resets, a form no seed holds), answered in
20
+ * the recorded `mcp-session-id` header; `notifications/initialized` on that session answers the
21
+ * recorded 202 and makes it ready; on a ready session `tools/list`, `tools/call`, and the standing
22
+ * `GET` (the recorded 405) answer the recordings with the session's id. At most 256 sessions are
23
+ * held: another `initialize` is refused before any fault. `reset` and `seed` clear the sessions.
24
+ * The seed only selects which recorded modern listing `tools/list` answers (`modernListing`:
25
+ * `one-page`, the default, or `two-pages`, whose first page issues the recorded cursor in the
26
+ * generation that first issues it, and `<cursor>.g<generation>` after a reset or seed; the second
27
+ * page answers only the cursor issued in the current generation). Every plan prepares its answer
28
+ * before anything is written; its commit (minting a session, readying one, issuing a cursor) runs
29
+ * only when no fault answers the request. Status faults (400-599) and `truncate-after-chunks`
30
+ * faults apply only after a request is admitted and planned, and a faulted request writes nothing:
31
+ * a truncation sends the prepared answer cut short and never runs the commit, so a truncated
32
+ * `initialize` holds no session and leaves the counter and the cap where they were, and a truncated
33
+ * first page issues no cursor. `match.route` selects one manifest row (a value naming no row is
34
+ * rejected when the fault is added); `match.method` is the HTTP method. Anything else answers one
35
+ * 400 not-emulated, ledgered with constant text only (`/<unrecognised>`, a standard method or
36
+ * `<other>`, an empty query, no headers or body, a constant reason, and the route template or row),
37
+ * that writes nothing and uses up no fault.
38
+ *
39
+ * Not emulated: `DELETE` (the client never sends it), `ping`, `resources/*`, `prompts/*`,
40
+ * `logging/*`, `completion/*`, `tasks/*`, JSON-RPC batches and client-sent responses, a JSON body
41
+ * repeating a key (the wrapper's opt-in `uniqueJsonKeys`), `mcp-*` headers no recording carries
42
+ * (such as `mcp-param-*`), cursors this emulator did not issue, any other tool or arguments, any
43
+ * other origin or path, and a missing `Authorization` (no fixture records the answer to one).
44
+ *
45
+ * Fail closed, on the shared wrapper's fail-closed mode as `/github`, `/google`, and
46
+ * `/linkedin-search` use it, with its opt-in `constantRefusals`, `guardAllHeaders`, and
47
+ * `guardOutput`: a request is recognised only on an emulated route shape, and any `Authorization`
48
+ * header is exactly `Bearer <token>` with a recognisable bearer (the RFC 6750 `b64token` syntax, at
49
+ * least 8 characters, a first character in `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, and a
50
+ * character outside `[0-9.eE+-]`). A recognised request that repeats the bearer anywhere (its raw
51
+ * path, the raw query or any query key or value, any request header name or value other than
52
+ * `Authorization`, or its body, through the wrapper's fixpoint closure of tolerant percent-decoding
53
+ * and JSON-unescaping, capped, where a cap refuses) is ledgered with constant text only. The
54
+ * prepared output is checked the same way before any fault is decided or anything is committed: an
55
+ * answer (every header and chunk) or a minted session id or cursor that would repeat the bearer is
56
+ * refused with the constant credential-repeat entry, no fault used and nothing written, so a bearer
57
+ * such as `yolk-emu-session-1`, or `synthetic-mcp` (inside the recorded `yolk-synthetic-mcp`),
58
+ * never reaches a response, the state, or `/_emulate/*`. The bearer is never stored, forwarded,
59
+ * ledgered, or echoed: routes see only its digest (the wrapper's opt-in `bearerDigest`; SHA-256 of
60
+ * the origin, a space, and the bearer), which they compare only with the digest of the public
61
+ * reserved invalid credential `yolk-conformance-invalid-credential-0000` (itself a recognisable
62
+ * bearer) to answer its recorded 401 byte for byte. Scope: the bearer is never copied from the
63
+ * request into a response, the state, or `/_emulate/*`; the output guard also refuses a prepared
64
+ * fixture answer, minted session id, or cursor that happens to contain it, but the emulator's other
65
+ * constants (state values such as `initializing`, wrapper headers such as `x-emulator-evidence`)
66
+ * and host-configured control-plane data (a fault body) may coincidentally equal a bearer and are
67
+ * not checked. `makeMcpEmulator` also throws when a copied recording is not canonical JSON (every
68
+ * JSON body and SSE `data:` payload equal to `JSON.stringify(JSON.parse(text))`), which id
69
+ * substitution relies on.
70
+ *
71
+ * Request-shape latitude (`/mcp`, the only accepted deviations): any bearer value in the RFC 6750
72
+ * `b64token` syntax (`[A-Za-z0-9\-._~+/]+=*`) of at least 8 characters, starting with a character
73
+ * in `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, with at least one outside `[0-9.eE+-]`, that
74
+ * occurs nowhere else in the request (any header name or value included) and in no answer or value
75
+ * the request would store (never stored or ledgered; only its digest is compared, with the digest
76
+ * of the public reserved invalid credential `yolk-conformance-invalid-credential-0000`, which
77
+ * answers the recorded 401 on the era probe); extra request headers, except `mcp-*` headers other
78
+ * than `mcp-method`, `mcp-name`, `mcp-protocol-version`, and `mcp-session-id`; a recorded header
79
+ * value sent as several headers that the HTTP layer joins into the recorded value; JSON key order;
80
+ * any JSON-RPC request id that is an integer from 0 to 2^53 - 1 or 1 to 64 printable ASCII
81
+ * characters where the recording has an id; any non-empty `name` and `version` (and no other key)
82
+ * in the `_meta` client info (`io.modelcontextprotocol/clientInfo`) of a modern request; a session
83
+ * id this emulator minted since the last reset or seed where the recording sends `mcp-session-id`
84
+ * (initializing for `notifications/initialized`, ready otherwise); and, with the seed's `two-pages`
85
+ * listing, the cursor this emulator issued in the current generation on the second page.
86
+ * `Authorization` must be exactly `Bearer <token>` (that spelling, one space). Everything else
87
+ * (another origin or path, any query, other HTTP methods such as `DELETE` or a `GET` on the modern
88
+ * profile, JSON-RPC methods no fixture of the profile records such as `ping`, `resources/*`, or
89
+ * `prompts/*`, batches and client-sent responses, other members, a JSON body repeating a key
90
+ * (compared after unescaping), a `null`, negative, or fractional id, other params (other tools,
91
+ * arguments, protocol versions, or capabilities, extra client-info keys, and a legacy `initialize`
92
+ * client info other than the recorded one), the MCP headers `accept`, `content-type`, `mcp-method`,
93
+ * `mcp-protocol-version`, `mcp-name`, and `last-event-id` other than the recorded values or present
94
+ * where none is recorded, any other `mcp-*` header (such as `mcp-param-*`), `mcp-session-id`
95
+ * missing where recorded or present where not, an unknown session or one in the wrong phase, a
96
+ * cursor not issued in the current generation, the reserved invalid credential on anything but the
97
+ * era probe, a bearer repeated anywhere in the request, and a bearer an answer or a minted session
98
+ * id or cursor would repeat) is not emulated.
99
+ *
100
+ * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
101
+ * `makeMcpEmulator` (importing this module has no side effects).
102
+ *
103
+ * @experimental
104
+ */
105
+ import { Data, Predicate } from 'effect'
106
+ import {
107
+ mcpApiRoutes,
108
+ mcpEmulatorDrillKnobs,
109
+ mcpRecordingProblems,
110
+ type McpApiEnv,
111
+ type McpEmulatorDrills
112
+ } from './mcp/api.ts'
113
+ import { mcpEmulatorFixtures } from './mcp/recordings.ts'
114
+ import {
115
+ buildSeedState,
116
+ decodeState,
117
+ makeMcpRuntime,
118
+ mcpBearerDigest,
119
+ mcpEmulatorOrigin,
120
+ mcpEmulatorReservedInvalidCredential,
121
+ type McpEmulatorSeed,
122
+ type McpEmulatorState
123
+ } from './mcp/state.ts'
124
+ import type { EmulatorRouteEvidence } from './route-evidence.ts'
125
+ import {
126
+ StatefulFaultMatch,
127
+ StatefulStreamFault,
128
+ checkBooleanDrills,
129
+ makeChunkedStatefulEmulator,
130
+ routeManifest,
131
+ type StatefulCoverage,
132
+ type StatefulEmulatorApi,
133
+ type StatefulFaultState,
134
+ type StatefulInputKind,
135
+ type StatefulLedgerEntry
136
+ } from './stateful-emulator.ts'
137
+ import { statefulCoreRuntime } from './stateful-core.ts'
138
+ import { isRecognisableBearerValue } from './stateful-secrets.ts'
139
+
140
+ export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
141
+
142
+ export { emulatorEvidenceHeader } from './route-evidence.ts'
143
+
144
+ export { isMcpEmulatedRequestId, mcpEmulatorDrillKnobs, type McpEmulatorDrills } from './mcp/api.ts'
145
+
146
+ export {
147
+ McpEmulatorModernListing,
148
+ McpEmulatorSeed,
149
+ McpEmulatorSession,
150
+ McpEmulatorStateSchema,
151
+ mcpEmulatorLegacyPath,
152
+ mcpEmulatorModernPath,
153
+ mcpEmulatorOrigin,
154
+ mcpEmulatorReservedInvalidCredential,
155
+ mcpEmulatorSessionCap,
156
+ mcpEmulatorSessionPrefix,
157
+ type McpEmulatorState
158
+ } from './mcp/state.ts'
159
+
160
+ export {
161
+ mcpEmulatorFixtures,
162
+ type McpRecordedExchange,
163
+ type McpRecordedFixture,
164
+ type McpRecordedRequest,
165
+ type McpRecordedResponse
166
+ } from './mcp/recordings.ts'
167
+
168
+ /**
169
+ * Route evidence manifest: one `RPC <origin><path>#<method>` row per recorded JSON-RPC method of
170
+ * each profile, plus the legacy `GET` row, with the conformance cases whose (synthetic,
171
+ * unverified) fixtures each answers. No row writes anything. Kept in sync with the handlers by
172
+ * construction (both come from one route table).
173
+ */
174
+ export const mcpEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> =
175
+ mcpApiRoutes.flatMap(routeManifest)
176
+
177
+ /**
178
+ * Optional fault filter; an omitted field matches every request. `method` is the HTTP method
179
+ * (`POST` or `GET`, never a row's `RPC`); `path` is the raw request path (ending in `*`, a
180
+ * prefix); `route` is a manifest row path (for example
181
+ * `https://mcp.example.test/legacy/mcp#tools/list`), and a value naming no row is rejected when
182
+ * the fault is added.
183
+ */
184
+ export const McpFaultMatch = StatefulFaultMatch
185
+
186
+ export type McpFaultMatch = typeof StatefulFaultMatch.Type
187
+
188
+ /**
189
+ * A status fault (400-599), or a `truncate-after-chunks` fault that sends the first `chunks`
190
+ * chunks of the prepared answer and then ends it cleanly (an SSE answer has two chunks, a JSON
191
+ * answer one). Only a request the emulator would answer reaches a fault: a request that is not
192
+ * emulated, by its shape or by the state, never uses one up. A faulted request writes nothing (a
193
+ * truncated `initialize` mints no session; a truncated first page issues no cursor). A truncation
194
+ * that cannot apply answers 500 and is not used up.
195
+ */
196
+ export const McpFault = StatefulStreamFault
197
+
198
+ export type McpFault = StatefulStreamFault
199
+
200
+ export type McpFaultState = StatefulFaultState<McpFault>
201
+
202
+ export type McpLedgerEntry = StatefulLedgerEntry
203
+
204
+ export type McpCoverage = StatefulCoverage
205
+
206
+ /** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
207
+ export class McpEmulatorInputInvalid extends Data.TaggedError('McpEmulatorInputInvalid')<{
208
+ readonly input: StatefulInputKind
209
+ readonly reason: string
210
+ }> {
211
+ override get message(): string {
212
+ return `Invalid MCP emulator ${this.input}: ${this.reason}`
213
+ }
214
+ }
215
+
216
+ export type McpEmulatorOptions = {
217
+ /** Typed seed; defaults to the one-page modern listing (sessions are never seeded). */
218
+ readonly seed?: McpEmulatorSeed
219
+ /** Drill knobs (tests only): make the emulator disagree with one conformance claim. */
220
+ readonly drills?: McpEmulatorDrills
221
+ }
222
+
223
+ export type McpEmulator = StatefulEmulatorApi<McpEmulatorState, McpEmulatorSeed, McpFault>
224
+
225
+ const inputInvalid = (input: StatefulInputKind, reason: string) =>
226
+ new McpEmulatorInputInvalid({ input, reason })
227
+
228
+ /** The constant reason of a request on no emulated route shape. */
229
+ const unrecognisedReason = 'no emulated MCP route for this method and path'
230
+
231
+ /** The constant reason of a request whose `Authorization` header is not one recognisable bearer. */
232
+ const unrecognisedAuthorizationReason = 'an unrecognisable Authorization header is not emulated'
233
+
234
+ /**
235
+ * Create a stateful MCP emulator on the `@emulators/core` custom runtime. Each call has its own
236
+ * state, ledger, faults, and session counter. Rejects with `McpEmulatorInputInvalid` for an
237
+ * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.
238
+ */
239
+ export const makeMcpEmulator = async (options: McpEmulatorOptions = {}): Promise<McpEmulator> => {
240
+ const initial = buildSeedState(options.seed ?? {})
241
+
242
+ if (Predicate.isString(initial)) {
243
+ throw inputInvalid('seed', initial)
244
+ }
245
+
246
+ checkBooleanDrills(options.drills, mcpEmulatorDrillKnobs, inputInvalid)
247
+
248
+ // The reserved invalid credential must reach the routes as a recognisable bearer's digest; a
249
+ // value the bearer rule refused would never be answered its recorded 401.
250
+ if (!isRecognisableBearerValue(mcpEmulatorReservedInvalidCredential)) {
251
+ throw new Error('the reserved invalid MCP credential is not a recognisable bearer')
252
+ }
253
+
254
+ // Id substitution re-serialises the response message: a recording copied in a non-canonical
255
+ // JSON form would answer other bytes, so it fails here, loudly.
256
+ const recordingProblems = mcpRecordingProblems(mcpEmulatorFixtures)
257
+
258
+ if (recordingProblems.length > 0) {
259
+ throw new Error(`non-canonical MCP recordings: ${recordingProblems.join('; ')}`)
260
+ }
261
+
262
+ const drills = options.drills ?? {}
263
+ const runtime = makeMcpRuntime()
264
+
265
+ const env: McpApiEnv = {
266
+ drills: {
267
+ discoverCarriesErrorResponse: drills.discoverCarriesErrorResponse === true,
268
+ discoverWithoutResultType: drills.discoverWithoutResultType === true,
269
+ sessionIdNotVisibleAscii: drills.sessionIdNotVisibleAscii === true,
270
+ discoverAnsweredTwice: drills.discoverAnsweredTwice === true,
271
+ writeToolMarkedReadOnly: drills.writeToolMarkedReadOnly === true,
272
+ readCallAnswersToolError: drills.readCallAnswersToolError === true,
273
+ invalidCallAnswersRpcError: drills.invalidCallAnswersRpcError === true,
274
+ absentCallAnswersResult: drills.absentCallAnswersResult === true,
275
+ unauthorizedWithoutChallenge: drills.unauthorizedWithoutChallenge === true
276
+ },
277
+ reservedDigest: mcpBearerDigest(mcpEmulatorReservedInvalidCredential, mcpEmulatorOrigin),
278
+ runtime
279
+ }
280
+
281
+ return makeChunkedStatefulEmulator<McpEmulatorState, McpApiEnv, McpEmulatorSeed>(
282
+ {
283
+ routes: mcpApiRoutes,
284
+ env,
285
+ initial,
286
+ buildSeed: buildSeedState,
287
+ recordHeaders: [
288
+ { name: 'accept', json: false },
289
+ { name: 'content-type', json: false },
290
+ { name: 'mcp-method', json: false },
291
+ { name: 'mcp-name', json: false },
292
+ { name: 'mcp-protocol-version', json: false },
293
+ { name: 'mcp-session-id', json: false }
294
+ ],
295
+ failClosed: {
296
+ unrecognised: unrecognisedReason,
297
+ unrecognisedAuthorization: unrecognisedAuthorizationReason
298
+ },
299
+ bearerDigest: mcpBearerDigest,
300
+ constantRefusals: true,
301
+ guardAllHeaders: true,
302
+ guardOutput: true,
303
+ uniqueJsonKeys: true,
304
+ // Every reset and seed starts a cursor generation; the session counter never resets.
305
+ clearRuntime: () => {
306
+ runtime.generation += 1
307
+ runtime.issuedCursor = undefined
308
+ },
309
+ runtimeState: () => ({
310
+ nextSession: runtime.nextSession,
311
+ cursorGeneration: runtime.generation,
312
+ issuedCursor: runtime.issuedCursor ?? null
313
+ }),
314
+ seedSummary: state => ({ modernListing: state.modernListing }),
315
+ inputInvalid
316
+ },
317
+ statefulCoreRuntime({ name: 'mcp', initial, decodeState, inputInvalid })
318
+ )
319
+ }