@gjermundgaraba/effect-actions 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
@@ -1,13 +1,8 @@
1
1
  # ActionHttp
2
2
 
3
- JSON `POST` routes on Effect's `HttpApi`, one per action. `ActionHttp.make` binds actions to a
4
- mount path; the server mounts that one binding, every client calls it and the OpenAPI document is
5
- generated from it.
3
+ JSON `POST` routes on Effect's `HttpApi`, one per action. `ActionHttp.make` binds actions to a mount path; the server mounts that one binding, every client calls it and the OpenAPI document is generated from it.
6
4
 
7
- A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search
8
- [ActionHttp-failures.md](ActionHttp-failures.md). For one symbol,
9
- `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc
10
- carries its rules and gotchas, and this page only what an example cannot show.
5
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionHttp-failures.md](ActionHttp-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
11
6
 
12
7
  ## Bind actions to routes
13
8
 
@@ -15,6 +10,7 @@ Call `ActionHttp.make` once, in a contract-only file, and export the binding:
15
10
 
16
11
  ```ts example=notes-service/src/Api.ts
17
12
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
13
+
18
14
  import { Login, WhoAmI } from "./identity/Identity.js";
19
15
  import { Notes } from "./notes/Notes.js";
20
16
  import { GetStatus } from "./status/Status.js";
@@ -29,25 +25,19 @@ export const Http = ActionHttp.make([GetStatus, ...Notes, WhoAmI], {
29
25
  });
30
26
  ```
31
27
 
32
- [quickstart/src/Api.ts](../examples/quickstart/src/Api.ts) binds public actions with no options;
33
- [kitchen-sink/src/Api.ts](../examples/kitchen-sink/src/Api.ts) gives `prefix` and `error` and makes
34
- two bindings, one per identity. What every endpoint declares is on `ActionHttp.make`, the rules of
35
- `prefix`, `error` and `authentication` on `ActionHttp.Options`, and a protected action's descriptor
36
- in [Authentication.md](Authentication.md#declare-how-callers-sign-in).
28
+ [quickstart/src/Api.ts](../examples/quickstart/src/Api.ts) binds public actions with no options; [kitchen-sink/src/Api.ts](../examples/kitchen-sink/src/Api.ts) gives `prefix` and `error` and makes two bindings, one per identity. What every endpoint declares is on `ActionHttp.make`, the rules of `prefix`, `error` and `authentication` on `ActionHttp.Options`, and a protected action's descriptor in [Authentication.md](Authentication.md#declare-how-callers-sign-in).
37
29
 
38
30
  ## Serve the routes
39
31
 
40
- Give `ActionHttp.layer` the binding and the implementations, merge it with the host's other
41
- routes, provide the descriptor's provider
42
- ([Authentication.md](Authentication.md#verify-credentials)), and serve the result with
43
- `HttpRouter.serve`.
32
+ Give `ActionHttp.layer` the binding and the implementations, merge it with the host's other routes, provide the descriptor's provider ([Authentication.md](Authentication.md#verify-credentials)), and serve the result with `HttpRouter.serve`.
44
33
 
45
34
  ```ts example=notes-service/src/routes.ts
35
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
36
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
46
37
  import { Layer } from "effect";
47
38
  import { HttpRouter, HttpServerResponse } from "effect/http";
48
39
  import { OpenApi } from "effect/http-api";
49
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
50
- import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
40
+
51
41
  import { Http } from "./Api.js";
52
42
  import { identity } from "./identity/handlers.js";
53
43
  import { Login } from "./identity/Identity.js";
@@ -70,7 +60,7 @@ const health = HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"));
70
60
  const openApi = HttpRouter.add(
71
61
  "GET",
72
62
  "/api/openapi.json",
73
- HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)),
63
+ HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api))
74
64
  );
75
65
 
76
66
  // What the host serves: the JSON routes, the same actions as MCP tools at `/mcp`, and the host's
@@ -85,12 +75,13 @@ const serving = (requestScoped: boolean) =>
85
75
  ActionMcp.layerHttp(implementations, {
86
76
  name: "notes-service",
87
77
  version: "1.0.0",
88
- instructions: "Notes belong to the signed-in user. Read a note before updating it.",
78
+ instructions:
79
+ "Notes belong to the signed-in user. Read a note before updating it.",
89
80
  authentication: Login,
90
81
  requestScoped,
91
82
  }),
92
83
  health,
93
- openApi,
84
+ openApi
94
85
  ).pipe(Layer.provide(authenticate));
95
86
 
96
87
  export const routes = serving(false);
@@ -98,17 +89,17 @@ export const routes = serving(false);
98
89
  export const requestScopedRoutes = serving(true);
99
90
  ```
100
91
 
101
- The smallest routes are [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts). What
102
- each route answers and what an `ErrorReporter` is given are on `ActionHttp.layer`.
92
+ The smallest routes are [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts). What each route answers and what an `ErrorReporter` is given are on `ActionHttp.layer`.
103
93
 
104
- The entrypoint serves the routes, with a request body limit; what that limit covers is on
105
- `ActionHttp.layer`.
94
+ The entrypoint serves the routes, with a request body limit; what that limit covers is on `ActionHttp.layer`.
106
95
 
107
96
  ```ts example=notes-service/src/server.ts
108
97
  import { createServer } from "node:http";
98
+
109
99
  import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
110
100
  import { ByteSize, Config, Effect, Layer } from "effect";
111
101
  import { HttpRouter, HttpServerRequest } from "effect/http";
102
+
112
103
  import { NoteStore } from "./notes/store.js";
113
104
  import { routes } from "./routes.js";
114
105
 
@@ -117,93 +108,122 @@ import { routes } from "./routes.js";
117
108
  const server = Layer.unwrap(
118
109
  Effect.gen(function* () {
119
110
  // The loopback default serves this machine alone; HOST=0.0.0.0 lets others reach it.
120
- const host = yield* Config.String("HOST").pipe(Config.withDefault("127.0.0.1"));
111
+ const host = yield* Config.String("HOST").pipe(
112
+ Config.withDefault("127.0.0.1")
113
+ );
114
+
121
115
  const port = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
122
116
 
123
117
  return HttpRouter.serve(routes).pipe(
124
118
  Layer.provide(NodeHttpServer.layer(createServer, { host, port })),
125
119
  // Request bodies have no size limit unless the host sets one. This covers every route that
126
120
  // reads its body whole.
127
- Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))),
121
+ Layer.provide(
122
+ Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))
123
+ ),
128
124
  // DEMO ONLY: notes live in this process's memory; a deployment provides a store that lasts.
129
- Layer.provide(NoteStore.layerMemory),
125
+ Layer.provide(NoteStore.layerMemory)
130
126
  );
131
- }),
127
+ })
132
128
  );
133
129
 
134
- Layer.launch(server).pipe(NodeRuntime.runMain);
130
+ Layer.launch(server).pipe(NodeRuntime.runMain());
135
131
  ```
136
132
 
137
- - A service a verifier, handler or authorizer reads per request is provided by router middleware
138
- around the layer
139
- ([kitchen-sink/src/workspaces/tenant.ts](../examples/kitchen-sink/src/workspaces/tenant.ts),
140
- read by the verifier, provided in
141
- [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts)), or, for a handler or
142
- authorizer, by `ActionHttp.LayerOptions.middleware`; declaring it, and why not
143
- `HttpRouter.provideRequest`, are on `ActionHttp.layer`.
133
+ - A service a verifier, handler or authorizer reads per request is provided by router middleware around the layer ([kitchen-sink/src/workspaces/tenant.ts](../examples/kitchen-sink/src/workspaces/tenant.ts), read by the verifier, provided in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts)), or, for a handler or authorizer, by `ActionHttp.LayerOptions.middleware`; declaring it, and why not `HttpRouter.provideRequest`, are on `ActionHttp.layer`.
144
134
 
145
135
  ### Split a binding across layers, and add middleware
146
136
 
147
- Serve each action from exactly one layer. Files:
148
- [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`api`),
149
- [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts). Where a
150
- check runs decides what it sees:
137
+ Serve each action from exactly one layer. Files: [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`api`), [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts). Where a check runs decides what it sees:
151
138
 
152
- | Where | Runs | Use it for |
153
- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- |
154
- | Global router middleware | before routing, on every route | a Host or Origin policy, CORS |
139
+ | Where | Runs | Use it for |
140
+ | --- | --- | --- |
141
+ | Global router middleware | before routing, on every route | a Host or Origin policy, CORS |
155
142
  | Router middleware provided to an `ActionHttp.layer` call | around its routes, before authentication | a per-request service; a limit on unauthenticated requests |
156
- | `ActionHttp.LayerOptions.middleware` | after authentication, before decoding | a limit keyed by the caller, counting invalid input too |
157
- | The handler ([Action.md](Action.md#authorize-calls)) | after decoding and the authorizer | a limit that holds on every surface, MCP included |
143
+ | `ActionHttp.LayerOptions.middleware` | after authentication, before decoding | a limit keyed by the caller, counting invalid input too |
144
+ | The handler ([Action.md](Action.md#authorize-calls)) | after decoding and the authorizer | a limit that holds on every surface, MCP included |
158
145
 
159
- What router middleware answering with a binding error must send is on `ActionHttp.Options.error`.
160
- A request policy that must run before authentication and OAuth discovery:
161
- [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
146
+ What router middleware answering with a binding error must send is on `ActionHttp.Options.error`. A request policy that must run before authentication and OAuth discovery: [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
162
147
 
163
148
  ### Publish the OpenAPI document
164
149
 
165
- Give `Http.api`, a plain `HttpApi`, to `OpenApi.fromApi`, `HttpApiSwagger.layer` or
166
- `HttpApiScalar.layer`, on routes of the host's own: `openApi` above,
167
- [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`documentation`). What the
168
- document holds and omits is on `ActionHttp.Binding.api`.
150
+ Give `Http.api`, a plain `HttpApi`, to `OpenApi.fromApi`, `HttpApiSwagger.layer` or `HttpApiScalar.layer`, on routes of the host's own: `openApi` above, [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`documentation`). What the document holds and omits is on `ActionHttp.Binding.api`.
169
151
 
170
152
  ## Call the API from Effect
171
153
 
172
- What a call fails with is on `ActionHttp.client`, and a token or a per-request sign-in goes
173
- through `ActionHttp.ClientOptions.transformClient`.
154
+ What a call fails with is on `ActionHttp.client`, and a token or a per-request sign-in goes through `ActionHttp.ClientOptions.transformClient`.
174
155
 
175
156
  ```ts example=quickstart/src/client.ts
176
157
  import { NodeRuntime } from "@effect/platform-node";
158
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
177
159
  import { Console, Effect } from "effect";
178
160
  import { FetchHttpClient } from "effect/http";
179
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
161
+
180
162
  import { Http } from "./Api.js";
181
163
 
182
164
  // Entrypoint: `node --import tsx examples/quickstart/src/client.ts`, with the server running.
183
165
  // One typed method per action of the binding.
184
166
  // DEMO ONLY: a deployed client reads its `baseUrl` from configuration (kitchen-sink/src/seed.ts).
185
167
  Effect.gen(function* () {
186
- const client = yield* ActionHttp.client(Http, { baseUrl: "http://127.0.0.1:3000" });
168
+ const client = yield* ActionHttp.client(Http, {
169
+ baseUrl: "http://127.0.0.1:3000",
170
+ });
187
171
 
188
172
  yield* Console.log(yield* client.greet({ name: "Ada" }));
189
- }).pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain);
173
+ }).pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain());
190
174
  ```
191
175
 
192
- [kitchen-sink/src/seed.ts](../examples/kitchen-sink/src/seed.ts) reads `baseUrl` from `Config`,
193
- sends headers and handles errors by tag. `ActionMcp.client` takes the same `transformClient`, and
194
- `ActionCli.remote` the unyielded `ActionHttp.client(Http, …)` as its `client`.
176
+ [kitchen-sink/src/seed.ts](../examples/kitchen-sink/src/seed.ts) reads `baseUrl` from `Config`, sends headers and handles errors by tag. `ActionMcp.client` takes the same `transformClient`, and `ActionCli.remote` the unyielded `ActionHttp.client(Http, …)` as its `client`.
177
+
178
+ - In a test, provide `Testing.layer(routes)` instead of a network client ([Testing.md](Testing.md#test-routes-in-memory), [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts)).
179
+
180
+ ## Serve and watch events
181
+
182
+ List [event types](Event.md) in the binding beside the actions, and their implementations in `layer`'s list, and in its `actions` where it is given: each is served at `POST <prefix>/events/<name>`, which answers a page of JSON, or an SSE stream to a request that accepts `text/event-stream`. Experimental, as [events](Event.md) are.
183
+
184
+ ```ts example=build-events/src/Api.ts
185
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
186
+
187
+ import {
188
+ BuildFinished,
189
+ FinishBuild,
190
+ PipelineStatus,
191
+ StartBuild,
192
+ TailLog,
193
+ } from "./builds/Builds.js";
194
+ import { DeployRequested, RequestDeploy } from "./deploys/Deploys.js";
195
+ import { Login } from "./identity/Identity.js";
196
+
197
+ // The HTTP binding: each action at `POST /api/<name>`, and each event at
198
+ // `POST /api/events/<name>`, which answers a page of JSON, or, asked for `text/event-stream`,
199
+ // holds a stream open. It imports no server code, so a client imports it with the contracts.
200
+ export const Http = ActionHttp.make(
201
+ [
202
+ FinishBuild,
203
+ StartBuild,
204
+ TailLog,
205
+ BuildFinished,
206
+ PipelineStatus,
207
+ RequestDeploy,
208
+ DeployRequested,
209
+ ],
210
+ { authentication: Login }
211
+ );
212
+ ```
213
+
214
+ [build-events/src/routes.ts](../examples/build-events/src/routes.ts) serves the same implementations over HTTP, MCP and RPC. A client of the binding has `watch(event, input, options)`, as `Action.client`'s: [build-events/test/http.test.ts](../examples/build-events/test/http.test.ts) watches by stream and by poll. What a route answers is on `layer`; what a watch fails with is on `client`. A protected event's binding also has `/subscribe` and `/unsubscribe` routes, for webhook delivery ([Webhook.md](Webhook.md)), which `make` says when it adds.
215
+
216
+ - **Not a browser `EventSource`.** It sends neither a JSON body nor a header credential, so a page watches with `fetchClient`, whose `watch` reads the stream through `fetch`.
217
+
218
+ - **Which deployment.** As over MCP, a log source and a `subscription` hook need one server that outlives a request.
219
+
220
+ ## Serve what another server serves
195
221
 
196
- - In a test, provide `Testing.layer(routes)` instead of a network client
197
- ([Testing.md](Testing.md#test-routes-in-memory),
198
- [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts)).
222
+ `ActionHttp.placed(binding, client)` stands, in a surface's list of implementations, for the binding's actions and event types where another server serves them, such as a Worker in front of the Durable Object that holds the data: each call and subscription is sent on, with the caller's headers, to the server `client` reaches for its input and its contract, which serves them in its own `ActionHttp.layer` of the binding, and its answer is passed back. `ActionHttp.layer`, `ActionMcp.layerHttp` and `ActionRpc.layer` take it beside their own implementations. Both servers verify the credential, so both are given the verifier; the server reached authorizes. Its rules are on `ActionHttp.placed`. An application doing it, a Worker sending each room's calls to its Durable Object, is `@gjermundgaraba/effect-actions-cloudflare`'s chat-rooms, through `ActionDurableObject.placed`: that package's `docs/ActionDurableObject-placement.md`.
199
223
 
200
224
  ## Call the API from a browser
201
225
 
202
- Code outside an Effect builds the client once with `ActionHttp.fetchClient` and runs each call
203
- with `Effect.runPromise`. Files:
204
- [notes-service/src/browser.ts](../examples/notes-service/src/browser.ts),
205
- [kitchen-sink/src/sdk.ts](../examples/kitchen-sink/src/sdk.ts) (a `baseUrl`, two headers).
226
+ Code outside an Effect builds the client once with `ActionHttp.fetchClient` and runs each call with `Effect.runPromise`. Files: [notes-service/src/browser.ts](../examples/notes-service/src/browser.ts), [kitchen-sink/src/sdk.ts](../examples/kitchen-sink/src/sdk.ts) (a `baseUrl`, two headers).
206
227
 
207
- - A page on another origin needs the server to allow it with `HttpRouter.cors`; sending cookies
208
- from there is on `ActionHttp.FetchClientOptions.fetch`.
228
+ - A page on another origin needs the server to allow it with `HttpRouter.cors`; sending cookies from there is on `ActionHttp.FetchClientOptions.fetch`.
209
229
  - Bundling the page: [setup.md](setup.md#bundle-for-a-browser).
@@ -9,7 +9,8 @@ What you see when something is wrong, and the fix. Search for a fragment of the
9
9
  - `subscriptions/listen` answered with JSON-RPC error `-32601`, Method not found: an endpoint without `features` has no 2026-07-28 subscriptions unless `allowSubscriptions` is `true`, and a `requestScoped` one has none. Pass `true` where a handler changes the server through `McpServer.McpServer`.
10
10
  - A request over HTTP answers 400, `MCP-Session-Id header is required; send initialize to start a session`: its `MCP-Protocol-Version` header is absent or names a 2025 revision, so it is a session's request, and it follows no `initialize`. One answering 404 names a session that ended, or one of a process since restarted: the client opens another.
11
11
  - A hand-written HTTP request answers 400 with JSON-RPC error `-32602`, `Required request metadata is missing` or `io.modelcontextprotocol/clientCapabilities request metadata is required`: it names 2026-07-28 in `MCP-Protocol-Version` and its `params._meta` lacks `io.modelcontextprotocol/protocolVersion`, for the first message, or `io.modelcontextprotocol/clientCapabilities`, for the second. Build it with `ActionMcp.request`, or copy the `curl` of [mcp-tools](../examples/mcp-tools/README.md#connect-a-client).
12
- - A hand-written HTTP request answers 400 with JSON-RPC error `-32020`, `MCP-Protocol-Version header is required`, `Mcp-Method header does not match request method` or `Mcp-Name header does not match request parameters`: those headers repeat a 2026-07-28 request's revision, method and `tools/call` name, and one is absent or differs from its body. Build it with `ActionMcp.request`.
12
+ - A hand-written HTTP request answers 400 with JSON-RPC error `-32020`, `MCP-Protocol-Version header is required`, `Mcp-Method header does not match request method` or `Mcp-Name header does not match request parameters`: those headers repeat a 2026-07-28 request's revision, method and `tools/call`, `events/poll`, `events/stream`, `events/subscribe` or `events/unsubscribe` name, and one is absent or differs from its body. Build it with `ActionMcp.request`.
13
+ - `events/list`, `events/poll` or `events/stream` answered with JSON-RPC error `-32601`, Method not found: the client opened a 2025 session with `initialize`, and events are served to 2026-07-28 requests alone. Send those, as `ActionMcp.client` does.
13
14
  - A hand-written HTTP request answers an empty 406: its `Accept` header does not list both `application/json` and `text/event-stream`, as `*/*` does not. Send both.
14
15
  - A request written by hand to a stdio server answers JSON-RPC error `-32602`, `Invalid request metadata`: it is neither preceded by `initialize` nor carries the 2026-07-28 metadata. Open with `initialize` and `notifications/initialized`, or give the request's `_meta` its `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`.
15
16
  - `Tool <name> has an output schema but did not return structured content` raised by a Python MCP client for an action that returns nothing: its success is sent as `null`, which a Python SDK release before the fix of python-sdk#3345 reads as missing. Update the SDK.
package/docs/ActionMcp.md CHANGED
@@ -1,48 +1,34 @@
1
1
  # ActionMcp
2
2
 
3
- MCP tools from implementations, on Effect's native `McpServer`: one tool per served action, named
4
- after it. `layerHttp` mounts a Streamable HTTP endpoint, for clients that connect to a URL;
5
- `runStdio` is the whole program of a subprocess a local agent starts. For a model your own code
6
- calls in process, use [ActionToolkit.md](ActionToolkit.md).
3
+ MCP tools from implementations, on Effect's native `McpServer`: one tool per served action, named after it. `layerHttp` mounts a Streamable HTTP endpoint, for clients that connect to a URL; `runStdio` is the whole program of a subprocess a local agent starts. For a model your own code calls in process, use [ActionToolkit.md](ActionToolkit.md).
7
4
 
8
- A message the library throws says its fix; for a compiler diagnostic or Effect's own error,
9
- search [ActionMcp-failures.md](ActionMcp-failures.md). For one symbol,
10
- `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc
11
- carries its rules and gotchas, and this page only what an example cannot show.
5
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionMcp-failures.md](ActionMcp-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
12
6
 
13
7
  ## Serve tools over HTTP
14
8
 
15
- Merge `layerHttp` into the routes, beside any other surface, and serve them with
16
- `HttpRouter.serve`:
9
+ Merge `layerHttp` into the routes, beside any other surface, and serve them with `HttpRouter.serve`:
17
10
 
18
11
  ```ts example=quickstart/src/routes.ts
19
- import { Layer } from "effect";
20
12
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
21
13
  import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
14
+ import { Layer } from "effect";
15
+
22
16
  import { Http } from "./Api.js";
23
17
  import { greeting } from "./greeting/handlers.js";
24
18
 
25
19
  // One implementation, two surfaces: a JSON route and an MCP tool at `/mcp`.
26
20
  export const routes = Layer.mergeAll(
27
21
  ActionHttp.layer(Http, greeting),
28
- ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" }),
22
+ ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" })
29
23
  );
30
24
  ```
31
25
 
32
- [quickstart/src/server.ts](../examples/quickstart/src/server.ts) serves them. Surfaces in one
33
- set of routes share each builder, as `Action.implement` says:
34
- [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts).
35
-
36
- - Claude Code 2.1 and Codex CLI 0.162 connect:
37
- `claude mcp add --transport http <name> <url>/mcp --header "Authorization: Bearer <token>"`,
38
- `codex mcp add <name> --url <url>/mcp --bearer-token-env-var <VAR>`;
39
- [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has stdio and Claude Desktop.
40
- - Where an endpoint without `authentication` listens, and its body limit:
41
- [mcp-tools/src/server.ts](../examples/mcp-tools/src/server.ts).
42
- - **On a serverless host**, such as Cloudflare Workers, pass `requestScoped: true`:
43
- [notes-service/src/worker.ts](../examples/notes-service/src/worker.ts).
44
- - The endpoint takes no layer middleware. A limit that must hold for tools goes in the handlers:
45
- [kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts).
26
+ [quickstart/src/server.ts](../examples/quickstart/src/server.ts) serves them. Surfaces in one set of routes share each builder, as `Action.implement` says: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts).
27
+
28
+ - Claude Code 2.1 and Codex CLI 0.162 connect: `claude mcp add --transport http <name> <url>/mcp --header "Authorization: Bearer <token>"`, `codex mcp add <name> --url <url>/mcp --bearer-token-env-var <VAR>`; [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has stdio and Claude Desktop.
29
+ - Where an endpoint without `authentication` listens, and its body limit: [mcp-tools/src/server.ts](../examples/mcp-tools/src/server.ts).
30
+ - **On a serverless host**, such as Cloudflare Workers, pass `requestScoped: true`: [notes-service/src/worker.ts](../examples/notes-service/src/worker.ts). For an endpoint in a Cloudflare Durable Object: the `@gjermundgaraba/effect-actions-cloudflare` package's docs.
31
+ - The endpoint takes no layer middleware. A limit that must hold for tools goes in the handlers: [kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts).
46
32
 
47
33
  ## Serve tools over stdio
48
34
 
@@ -50,9 +36,10 @@ Provide `Stdio` and the startup services, then run it:
50
36
 
51
37
  ```ts example=notes-service/src/stdio.ts
52
38
  import { NodeRuntime, NodeStdio } from "@effect/platform-node";
53
- import { Effect } from "effect";
54
39
  import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
55
40
  import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
41
+ import { Effect } from "effect";
42
+
56
43
  import { identity } from "./identity/handlers.js";
57
44
  import { CurrentUser } from "./identity/Identity.js";
58
45
  import { users } from "./identity/users.js";
@@ -68,98 +55,139 @@ import { status } from "./status/handlers.js";
68
55
  // over stdio, so this host names the caller itself. It is a process of its own with services
69
56
  // of its own. DEMO ONLY: this in-memory store holds its notes, not the running server's. To
70
57
  // share them, both provide a store they both reach, such as a database.
71
- ActionMcp.runStdio([status, notes, identity], { name: "notes-service", version: "1.0.0" }).pipe(
58
+ ActionMcp.runStdio([status, notes, identity], {
59
+ name: "notes-service",
60
+ version: "1.0.0",
61
+ }).pipe(
72
62
  // DEMO ONLY: a real host reads who started it, from its own login or configuration.
73
63
  Effect.provideService(CurrentUser, users.ada),
74
64
  Effect.provide([NodeStdio.layer, NoteStore.layerMemory]),
75
65
  // stdout carries the protocol alone, so logs and failures go to stderr.
76
66
  ActionCli.logToStderr,
77
- NodeRuntime.runMain,
67
+ NodeRuntime.runMain()
78
68
  );
79
69
  ```
80
70
 
81
- [mcp-tools/src/stdio.ts](../examples/mcp-tools/src/stdio.ts) serves public tools. What
82
- `ActionCli.logToStderr` moves, and what it cannot, is on `ActionCli.logToStderr`. Test the tools
83
- without a process: [Testing.md](Testing.md#test-mcp-tools).
71
+ [mcp-tools/src/stdio.ts](../examples/mcp-tools/src/stdio.ts) serves public tools. What `ActionCli.logToStderr` moves, and what it cannot, is on `ActionCli.logToStderr`. Test the tools without a process: [Testing.md](Testing.md#test-mcp-tools).
84
72
 
85
- To try a server by hand, send one request as `ActionMcp.request` builds it, its 2026-07-28
86
- metadata in `_meta`: [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has the
87
- `curl`, with HTTP's five headers, and
88
- [notes-service](../examples/notes-service/README.md#run-it) the stdio command.
73
+ To try a server by hand, send one request as `ActionMcp.request` builds it, its 2026-07-28 metadata in `_meta`: [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has the `curl`, with HTTP's five headers, and [notes-service](../examples/notes-service/README.md#run-it) the stdio command.
89
74
 
90
75
  ## Describe tools to a model
91
76
 
92
- A tool is its contract ([Action.md](Action.md#define-a-contract)): a model knows only the action's
93
- name, its `description`, and its input schema with each field's `description`; how the tool
94
- presents itself is `Action.Options`'s `mcp`. The server's `instructions` say what the tools are
95
- for together. See
96
- [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts) and
97
- [mcp-tools/src/instructions.ts](../examples/mcp-tools/src/instructions.ts).
98
-
99
- - Constrain in the schema what a model may send: `PrinterId` in
100
- [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts). Which
101
- checks reach the listed schema is on `Action.Options.input`.
102
- - A model acts on a declared error's `message`, so write it for the model:
103
- [mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts).
104
- - `ActionMcp.Options`'s `actions` lists the tools, and only listed actions must suit MCP:
105
- [kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts). Leave out an action a
106
- model should not call, or one that takes a secret ([Action.md](Action.md#define-a-contract)).
77
+ A tool is its contract ([Action.md](Action.md#define-a-contract)): a model knows only the action's name, its `description`, and its input schema with each field's `description`; how the tool presents itself is `Action.Options`'s `mcp`. The server's `instructions` say what the tools are for together. See [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts) and [mcp-tools/src/instructions.ts](../examples/mcp-tools/src/instructions.ts).
78
+
79
+ - Constrain in the schema what a model may send: `PrinterId` in [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts). Which checks reach the listed schema is on `Action.Options.input`.
80
+ - A model acts on a declared error's `message`, so write it for the model: [mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts).
81
+ - `ActionMcp.Options`'s `actions` lists the tools, and only listed actions must suit MCP: [kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts). Leave out an action a model should not call, or one that takes a secret ([Action.md](Action.md#define-a-contract)).
107
82
 
108
83
  ## Return images and other media
109
84
 
110
- Give the success an `Action.Image` field and return its bytes and MIME type from the handler:
111
- `get_camera_snapshot` in
112
- [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts);
113
- `ActionMcp.client` decodes it back. Where `ActionMcp` refuses one is on `Action.Image`:
114
- [kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts).
85
+ Give the success an `Action.Image` field and return its bytes and MIME type from the handler: `get_camera_snapshot` in [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts); `ActionMcp.client` decodes it back. Where `ActionMcp` refuses one is on `Action.Image`: [kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts).
115
86
 
116
87
  ## Protect tools
117
88
 
118
- Name the protected actions' authentication descriptor as `authentication` and provide its
119
- provider to the routes: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts).
120
- Write the [descriptor](Authentication.md#declare-how-callers-sign-in) and its
121
- [provider](Authentication.md#verify-credentials) once for every surface, and
122
- [publish discovery](Authentication.md#publish-oauth-discovery) so an MCP client finds where to
123
- sign in. What a signed-in caller may do is each implementation's
124
- [`authorize`](Action.md#authorize-calls), which runs on every call of a protected tool.
89
+ Name the protected actions' authentication descriptor as `authentication` and provide its provider to the routes: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). Write the [descriptor](Authentication.md#declare-how-callers-sign-in) and its [provider](Authentication.md#verify-credentials) once for every surface, and [publish discovery](Authentication.md#publish-oauth-discovery) so an MCP client finds where to sign in. What a signed-in caller may do is each implementation's [`authorize`](Action.md#authorize-calls), which runs on every call of a protected tool.
90
+
91
+ - **One endpoint or two.** What a mixed endpoint, serving public and protected tools, shows and runs signed out is on `ActionMcp.LayerHttpOptions`'s `authentication`.
92
+ - Test as a signed-in caller ([Testing.md](Testing.md#test-as-a-signed-in-caller)): [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts).
93
+
94
+ ## Serve events
95
+
96
+ List [event implementations](Event.md) beside the actions, and their event types in `actions` where it is given: `layerHttp` and `runStdio` serve them as MCP events, SEP-3415's `events/list`, `events/poll` and `events/stream`, to 2026-07-28 clients. Experimental, as [events](Event.md) are.
97
+
98
+ ```ts example=build-events/src/routes.ts
99
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
100
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
101
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
102
+ import * as Webhook from "@gjermundgaraba/effect-actions/Webhook";
103
+ import { Layer } from "effect";
104
+ import { RpcSerialization, RpcServer } from "effect/rpc";
105
+
106
+ import { Http } from "./Api.js";
107
+ import { buildEvents } from "./builds/events.js";
108
+ import { builds } from "./builds/handlers.js";
109
+ import { logs } from "./builds/log.js";
110
+ import { statusEvents } from "./builds/status.js";
111
+ import { deployEvents } from "./deploys/events.js";
112
+ import { deploys } from "./deploys/handlers.js";
113
+ import { Login } from "./identity/Identity.js";
114
+ import { authenticate } from "./identity/verifier.js";
115
+ import { Rpc } from "./Rpc.js";
116
+
117
+ // Every domain's implementation, once: the events beside the actions, in one list.
118
+ const implementations = [
119
+ builds,
120
+ logs,
121
+ buildEvents,
122
+ statusEvents,
123
+ deploys,
124
+ deployEvents,
125
+ ];
126
+
127
+ // The actions as MCP tools and the events as MCP events, at `/mcp`. A client polls with
128
+ // `events/poll`, or holds `events/stream` open and is pushed each occurrence. Both events are
129
+ // protected, so each request needs a member's token, and each subscriber sees what its token
130
+ // lets it. This long-lived process holds the deploy event's `subscription` hook between polls and
131
+ // its log in memory, and pushes each occurrence: a `requestScoped` endpoint would serve the events
132
+ // by poll and webhook alone, and a serverless host, where no process lasts, check sources alone,
133
+ // without `Webhook.layer`.
134
+ //
135
+ // A tool answers one result, so `tailLog` would collect a whole log before answering, all of it
136
+ // in a model's context: its implementation, `logs`, is left out, and the endpoint serves every
137
+ // other action and every event.
138
+ const mcp = ActionMcp.layerHttp(
139
+ [builds, buildEvents, statusEvents, deploys, deployEvents],
140
+ {
141
+ name: "build-events",
142
+ version: "1.0.0",
143
+ authentication: Login,
144
+ }
145
+ );
146
+
147
+ // The same implementations as routes under `/api`, for a client that speaks HTTP rather than
148
+ // MCP: one implementation of each event serves every surface.
149
+ const api = ActionHttp.layer(Http, implementations);
150
+
151
+ // And as Effect RPC at `/rpc`, over HTTP. A watch is a stream rpc that never ends, so the
152
+ // serialization frames each element as it is sent: `layerNdjson` does, where `layerJson` would
153
+ // answer once every stream of the request ended, which a watch never does.
154
+ const rpc = ActionRpc.layer(Rpc, implementations).pipe(
155
+ Layer.provide(RpcServer.layerProtocolHttp({ path: "/rpc" })),
156
+ Layer.provide(RpcSerialization.layerNdjson)
157
+ );
158
+
159
+ // Webhook delivery for MCP and HTTP alike: a member names a callback URL and a secret, with
160
+ // MCP's `events/subscribe` or `POST /api/events/<name>/subscribe`, and each occurrence it may see
161
+ // is POSTed there, signed. One layer holds the subscriptions of both, so either surface refreshes
162
+ // one made over the other. `persist` keeps each in a `KeyValueStore`, so a restart goes on
163
+ // delivering from where it reached, as the subscriber `Login`'s `schema` keeps. It needs an
164
+ // `HttpClient` to POST, `Dns` to check where each callback resolves, and the store, which
165
+ // `server.ts` provides.
166
+ export const routes = Layer.mergeAll(mcp, api, rpc).pipe(
167
+ Layer.provide(authenticate),
168
+ Layer.provide(Webhook.layer({ persist: true }))
169
+ );
170
+ ```
171
+
172
+ [build-events/test/routes.test.ts](../examples/build-events/test/routes.test.ts) watches both events with `client`, by stream and by poll. What each method answers, and when the endpoint refuses to start, is on `layerHttp`; what a watch fails with is on `client`.
173
+
174
+ Webhook delivery, `events/subscribe` and `events/unsubscribe`, is `Webhook.layer`'s ([Webhook.md](Webhook.md)), provided to the endpoint.
125
175
 
126
- - **One endpoint or two.** What a mixed endpoint, serving public and protected tools, shows and
127
- runs signed out is on `ActionMcp.LayerHttpOptions`'s `authentication`.
128
- - Test as a signed-in caller ([Testing.md](Testing.md#test-as-a-signed-in-caller)):
129
- [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts).
176
+ - **Which deployment.** Push needs a server that outlives a request, so a `requestScoped` endpoint serves events by poll, and by webhook where `Webhook.layer` is provided. Sources, logs, `subscription` hooks' leases and webhook subscriptions are built once with the endpoint, either way. On a host with no lasting process, such as a serverless platform, serve check sources alone, without a `subscription` hook or `Webhook.layer`: nothing refuses the others, and under `Event.layerMemory` each instance keeps its own log of a log source, whose cursors another reads as a gap, and its own leases and webhook subscriptions, lost as it ends.
130
177
 
131
178
  ## Allow browser clients
132
179
 
133
- A browser MCP client sends its page's `Origin`. List the origins it may come from as
134
- `allowedOrigins`, and add CORS where one is configured:
135
- [mcp-tools/src/routes.ts](../examples/mcp-tools/src/routes.ts), and
136
- [kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts) with
137
- authentication.
180
+ A browser MCP client sends its page's `Origin`. List the origins it may come from as `allowedOrigins`, and add CORS where one is configured: [mcp-tools/src/routes.ts](../examples/mcp-tools/src/routes.ts), and [kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts) with authentication.
138
181
 
139
- - Authentication runs before the Origin check, so a request from an unlisted origin that must
140
- authenticate, or sends a token, gets 401 without a valid one, not 403.
141
- - `allowedOrigins` checks no `Host`, and a request sending no `Origin` passes. A `Host`
142
- allowlist, or an Origin check that must run first, is a request policy:
143
- [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
182
+ - Authentication runs before the Origin check, so a request from an unlisted origin that must authenticate, or sends a token, gets 401 without a valid one, not 403.
183
+ - `allowedOrigins` checks no `Host`, and a request sending no `Origin` passes. A `Host` allowlist, or an Origin check that must run first, is a request policy: [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
144
184
 
145
185
  ## Add native prompts and resources
146
186
 
147
- Merge Effect's own `McpServer.resource`, `McpServer.prompt` and `McpServer.toolkit` layers into
148
- one and pass it as `features`:
149
- [kitchen-sink/src/issues/guide.ts](../examples/kitchen-sink/src/issues/guide.ts), given in
150
- [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and
151
- [kitchen-sink/src/stdio.ts](../examples/kitchen-sink/src/stdio.ts). Where their services
152
- come from is on `ActionMcp.Options`'s `features`, and when they need a credential on
153
- `ActionMcp.LayerHttpOptions`'s `authentication`.
187
+ Merge Effect's own `McpServer.resource`, `McpServer.prompt` and `McpServer.toolkit` layers into one and pass it as `features`: [kitchen-sink/src/issues/guide.ts](../examples/kitchen-sink/src/issues/guide.ts), given in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and [kitchen-sink/src/stdio.ts](../examples/kitchen-sink/src/stdio.ts). Where their services come from is on `ActionMcp.Options`'s `features`, and when they need a credential on `ActionMcp.LayerHttpOptions`'s `authentication`.
154
188
 
155
189
  ## Send notifications from a handler
156
190
 
157
- A handler yields `McpServer.McpServer` and sends through it:
158
- `server.notifications["notifications/progress"]({ progressToken, progress })`.
159
- `McpSchema.McpRequestContext` holds what the client sent: its `clientInfo`, and the request's
160
- `_meta` as `requestMetadata`. The endpoint and `runStdio` provide both; an implementation also
161
- served on another surface reads them with `Effect.serviceOption`.
191
+ A handler yields `McpServer.McpServer` and sends through it: `server.notifications["notifications/progress"]({ progressToken, progress })`. `McpSchema.McpRequestContext` holds what the client sent: its `clientInfo`, and the request's `_meta` as `requestMetadata`. The endpoint and `runStdio` provide both; an implementation also served on another surface reads them with `Effect.serviceOption`.
162
192
 
163
- - On 2026-07-28, a progress notification is sent only with the token its request carried,
164
- `requestMetadata?.progressToken`. With another, or when the client sent none, it is dropped
165
- silently.
193
+ - On 2026-07-28, a progress notification is sent only with the token its request carried, `requestMetadata?.progressToken`. With another, or when the client sent none, it is dropped silently.