@gjermundgaraba/effect-actions 0.18.2 → 0.19.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 (144) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -47
  3. package/dist/Action.mjs +11 -14
  4. package/dist/ActionCli.d.mts +81 -42
  5. package/dist/ActionCli.mjs +52 -33
  6. package/dist/ActionHttp.d.mts +84 -43
  7. package/dist/ActionHttp.mjs +2 -2
  8. package/dist/ActionMcp.d.mts +54 -27
  9. package/dist/ActionMcp.mjs +5 -5
  10. package/dist/ActionRpc.d.mts +48 -35
  11. package/dist/ActionRpc.mjs +1 -1
  12. package/dist/ActionToolkit.d.mts +32 -13
  13. package/dist/ActionToolkit.mjs +10 -7
  14. package/dist/Authentication.d.mts +92 -40
  15. package/dist/Authentication.mjs +37 -35
  16. package/dist/Testing.d.mts +22 -24
  17. package/dist/authentication/provider.d.mts +1 -4
  18. package/dist/authentication/provider.mjs +1 -1
  19. package/dist/authentication/refusal.mjs +2 -1
  20. package/dist/cli/commands.d.mts +34 -11
  21. package/dist/cli/commands.mjs +47 -22
  22. package/dist/contract/caller.mjs +1 -1
  23. package/dist/contract/declared.mjs +27 -14
  24. package/dist/contract/errors.d.mts +42 -17
  25. package/dist/contract/errors.mjs +42 -17
  26. package/dist/contract/hints.mjs +3 -2
  27. package/dist/contract/implementation.d.mts +5 -8
  28. package/dist/contract/implementation.mjs +29 -17
  29. package/dist/contract/media.d.mts +6 -2
  30. package/dist/contract/media.mjs +6 -2
  31. package/dist/contract/names.mjs +14 -12
  32. package/dist/contract/status.mjs +3 -10
  33. package/dist/http/client.d.mts +15 -4
  34. package/dist/http/client.mjs +1 -1
  35. package/dist/mcp/client.d.mts +21 -8
  36. package/dist/mcp/client.mjs +6 -8
  37. package/docs/Action-failures.md +6 -15
  38. package/docs/Action.md +159 -191
  39. package/docs/ActionCli-failures.md +12 -92
  40. package/docs/ActionCli.md +102 -156
  41. package/docs/ActionHttp-failures.md +14 -76
  42. package/docs/ActionHttp.md +150 -176
  43. package/docs/ActionMcp-failures.md +2 -22
  44. package/docs/ActionMcp.md +79 -170
  45. package/docs/ActionRpc-failures.md +3 -12
  46. package/docs/ActionRpc.md +183 -120
  47. package/docs/ActionToolkit-failures.md +3 -10
  48. package/docs/ActionToolkit.md +112 -109
  49. package/docs/Authentication-advanced.md +167 -108
  50. package/docs/Authentication-failures.md +11 -91
  51. package/docs/Authentication.md +114 -135
  52. package/docs/README.md +45 -30
  53. package/docs/Testing-failures.md +2 -6
  54. package/docs/Testing.md +126 -159
  55. package/docs/setup-failures.md +8 -32
  56. package/docs/setup.md +42 -37
  57. package/examples/README.md +6 -10
  58. package/examples/assistant/README.md +20 -38
  59. package/examples/assistant/src/agent.ts +2 -3
  60. package/examples/assistant/src/cli.ts +4 -9
  61. package/examples/assistant/src/commands.ts +6 -4
  62. package/examples/assistant/src/identity/authorize.ts +3 -3
  63. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  64. package/examples/assistant/src/tasks/handlers.ts +3 -3
  65. package/examples/assistant/src/toolkit.ts +6 -8
  66. package/examples/assistant/test/commands.test.ts +16 -11
  67. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  68. package/examples/assistant/test/toolkit.test.ts +0 -1
  69. package/examples/kitchen-sink/README.md +30 -52
  70. package/examples/kitchen-sink/src/Actions.ts +2 -3
  71. package/examples/kitchen-sink/src/Api.ts +3 -4
  72. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  73. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  74. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  75. package/examples/kitchen-sink/src/agent.ts +1 -1
  76. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  77. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  78. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  79. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  80. package/examples/kitchen-sink/src/browser.ts +3 -4
  81. package/examples/kitchen-sink/src/cli.ts +6 -7
  82. package/examples/kitchen-sink/src/config.ts +2 -0
  83. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  84. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  85. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  86. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  87. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  88. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  89. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  90. package/examples/kitchen-sink/src/ops.ts +3 -6
  91. package/examples/kitchen-sink/src/request-policy.ts +3 -3
  92. package/examples/kitchen-sink/src/routes.ts +4 -6
  93. package/examples/kitchen-sink/src/seed.ts +0 -1
  94. package/examples/kitchen-sink/src/server.ts +11 -12
  95. package/examples/kitchen-sink/src/stdio.ts +3 -2
  96. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  97. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  98. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  99. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  100. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  101. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  102. package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
  103. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  104. package/examples/live-rpc/README.md +8 -49
  105. package/examples/live-rpc/src/browser.ts +8 -41
  106. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  107. package/examples/live-rpc/src/chat/room.ts +1 -1
  108. package/examples/live-rpc/src/client.ts +2 -3
  109. package/examples/live-rpc/src/page.ts +34 -0
  110. package/examples/live-rpc/src/routes.ts +6 -7
  111. package/examples/live-rpc/src/server.ts +6 -4
  112. package/examples/live-rpc/test/browser.test.ts +6 -5
  113. package/examples/live-rpc/test/routes.test.ts +16 -15
  114. package/examples/live-rpc/test/server.test.ts +1 -1
  115. package/examples/mcp-tools/README.md +22 -45
  116. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  117. package/examples/mcp-tools/src/routes.ts +0 -7
  118. package/examples/mcp-tools/src/server.ts +1 -1
  119. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  120. package/examples/mcp-tools/test/routes.test.ts +2 -3
  121. package/examples/notes-service/README.md +9 -43
  122. package/examples/notes-service/src/cli.ts +0 -3
  123. package/examples/notes-service/src/commands.ts +7 -17
  124. package/examples/notes-service/src/identity/authorize.ts +2 -5
  125. package/examples/notes-service/src/identity/verifier.ts +3 -4
  126. package/examples/notes-service/src/notes/Notes.ts +2 -6
  127. package/examples/notes-service/src/notes/store.ts +5 -1
  128. package/examples/notes-service/src/routes.ts +6 -8
  129. package/examples/notes-service/src/server.ts +4 -3
  130. package/examples/notes-service/src/stdio.ts +4 -3
  131. package/examples/notes-service/test/commands.test.ts +1 -1
  132. package/examples/notes-service/test/notes/as.ts +6 -0
  133. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  134. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  135. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  136. package/examples/notes-service/test/routes.test.ts +2 -2
  137. package/examples/quickstart/README.md +1 -1
  138. package/examples/quickstart/src/client.ts +1 -0
  139. package/examples/quickstart/src/server.ts +2 -4
  140. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  141. package/package.json +1 -1
  142. package/docs/Action-in-process.md +0 -93
  143. package/docs/CONTEXT.md +0 -46
  144. package/docs/guarantees.md +0 -44
@@ -4,72 +4,36 @@ JSON `POST` routes on Effect's `HttpApi`, one per action. `ActionHttp.make` bind
4
4
  mount path; the server mounts that one binding, every client calls it and the OpenAPI document is
5
5
  generated from it.
6
6
 
7
- Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
8
- When something fails, search [ActionHttp-failures.md](ActionHttp-failures.md). Reading order:
9
- [README.md](README.md).
10
-
11
- ## API
12
-
13
- Import `@gjermundgaraba/effect-actions/ActionHttp`.
14
-
15
- | API | Purpose |
16
- | ---------------------------------------- | ----------------------------------------------------------------------- |
17
- | `make(actions, options?)` | Bind a list of actions; returns a `Binding`. |
18
- | `Http.actions` | The bound actions. |
19
- | `Http.error` | The errors every endpoint declares besides its action's own, as a list. |
20
- | `Http.prefix` | Where the routes mount. |
21
- | `Http.authentication` | Its protected actions' authentication descriptor, or `undefined`. |
22
- | `Http.api` | The native `HttpApi`, for OpenAPI and Effect's own tools. |
23
- | `layer(Http, implementations, options?)` | Mount the routes of the bound actions the implementations hold. |
24
- | `client(Http, options?)` | An Effect of a typed client; requires the native `HttpClient`. |
25
- | `fetchClient(Http, options?)` | The same client over `fetch`, built outside an Effect. |
26
-
27
- Exported types: `Binding`; `Any`, any binding; `Client<typeof Http>`, a client's type;
28
- `MethodError`, what one of its calls fails with; `Options` of `make`, `LayerOptions`,
29
- `ClientOptions` and `FetchClientOptions`.
30
-
31
- | Option | Meaning |
32
- | --------------------------- | ------------------------------------------------------------------------------------------------- |
33
- | `make`: `prefix` | Mount path of every route; defaults to `/api`. `/` mounts at the root. |
34
- | `make`: `error` | What middleware around the routes answers with, one schema or a list; every endpoint declares it. |
35
- | `make`: `authentication` | The protected actions' descriptor, `Authentication.make(...)`; required with a protected action. |
36
- | `layer`: `actions` | The bound actions it serves: `[GetUser]`. Defaults to every one the implementations hold. |
37
- | `layer`: `middleware` | Native `HttpApiMiddleware` around every route it serves, the first innermost. |
38
- | `client`: `baseUrl` | What routes resolve against: `https://api.example.com`. Omitted: relative ones, a page's origin. |
39
- | `client`: `transformClient` | Wraps the native `HttpClient`: a token, or a sign-in per request, whose failures each call adds. |
40
- | `fetchClient`: `fetch` | Its `fetch`, beside `client`'s options. Omitted: the global `fetch`, looked up on every call. |
41
-
42
- Each action is served at `POST <prefix>/<action>`, a `readOnly` one too. One without `success`
43
- answers an empty 200, with no body and no content type, so a hand-written client must not parse
44
- it. One without `input` still takes the body `{}`, typed as JSON. Statuses and bodies:
45
- [guarantees.md](guarantees.md#wire-behavior).
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.
46
11
 
47
12
  ## Bind actions to routes
48
13
 
49
- Call `ActionHttp.make` once, in a file that imports contracts and no server code, and export the
50
- binding: the server, every client, the CLI and the tests import that one value.
14
+ Call `ActionHttp.make` once, in a contract-only file, and export the binding:
51
15
 
52
- ```ts
16
+ ```ts example=notes-service/src/Api.ts
17
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
18
+ import { Login, WhoAmI } from "./identity/Identity.js";
19
+ import { Notes } from "./notes/Notes.js";
20
+ import { GetStatus } from "./status/Status.js";
21
+
22
+ // The HTTP binding: every action at `POST /api/<name>`, the protected ones requiring `Login`.
23
+ // It is plain data and imports no server code, so the server, the CLI and a browser page all
24
+ // import this one value, and each gets its routes or its typed methods from it. An action
25
+ // added here is also a `notes` command: commands.ts derives one per action of this binding.
53
26
  export const Http = ActionHttp.make([GetStatus, ...Notes, WhoAmI], {
54
27
  prefix: "/api",
55
28
  authentication: Login,
56
29
  });
57
30
  ```
58
31
 
59
- Files: [quickstart/src/Api.ts](../examples/quickstart/src/Api.ts) (public actions, no options),
60
- [notes-service/src/Api.ts](../examples/notes-service/src/Api.ts) (protected actions),
61
- [kitchen-sink/src/Api.ts](../examples/kitchen-sink/src/Api.ts) (`prefix`, `error`, two bindings).
62
-
63
- - To keep an action off HTTP, leave it out of the binding; its implementation may still serve
64
- it as a tool or a command.
65
- - A binding holding a protected action names its descriptor
66
- ([Authentication.md](Authentication.md#declare-how-callers-sign-in)), and has only one: a
67
- second identity gets a second binding with its own `prefix`. A public action may be in both.
68
- - `error` declares what middleware answers with on any route, such as a quota's 429. An error a
69
- handler fails with goes in its action's `error`
70
- ([Action.md](Action.md#fail-with-declared-and-built-in-errors)).
71
- - Action names are unique within a binding. Split a large API, or two actions sharing a name,
72
- into one binding per area, each with its own `prefix` and client; one host serves them all.
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).
73
37
 
74
38
  ## Serve the routes
75
39
 
@@ -78,102 +42,146 @@ routes, provide the descriptor's provider
78
42
  ([Authentication.md](Authentication.md#verify-credentials)), and serve the result with
79
43
  `HttpRouter.serve`.
80
44
 
81
- ```ts
45
+ ```ts example=notes-service/src/routes.ts
46
+ import { Layer } from "effect";
47
+ import { HttpRouter, HttpServerResponse } from "effect/http";
48
+ import { OpenApi } from "effect/http-api";
49
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
50
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
51
+ import { Http } from "./Api.js";
52
+ import { identity } from "./identity/handlers.js";
53
+ import { Login } from "./identity/Identity.js";
54
+ import { authenticate } from "./identity/verifier.js";
55
+ import { notes } from "./notes/handlers.js";
56
+ import { status } from "./status/handlers.js";
57
+
58
+ // Every domain's implementation, once. Each surface below takes the same list.
59
+ const implementations = [status, notes, identity];
60
+
61
+ // JSON routes at `/api/<action>`, the binding's; `getStatus` answers anyone.
82
62
  const api = ActionHttp.layer(Http, implementations);
83
63
 
84
- export const routes = Layer.mergeAll(api, mcp, health, openApi).pipe(Layer.provide(authenticate));
64
+ // The host's own routes sit beside the actions' on the same router.
65
+ const health = HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"));
66
+
67
+ // Served to anyone, which is a choice: it tells a signed-out caller every action's name and
68
+ // shape. To keep them, leave the route out, or provide `Authentication.protect(Login).layer`
69
+ // to it.
70
+ const openApi = HttpRouter.add(
71
+ "GET",
72
+ "/api/openapi.json",
73
+ HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)),
74
+ );
75
+
76
+ // What the host serves: the JSON routes, the same actions as MCP tools at `/mcp`, and the host's
77
+ // own routes. The MCP endpoint serves the public `getStatus` beside the protected tools, so a
78
+ // signed-out 2026-07-28 client may list the tools and call `getStatus`; what else needs a token
79
+ // is on `ActionMcp.LayerHttpOptions.authentication`. It still requires `NoteStore`: the
80
+ // entrypoint chooses which one, and a test chooses its own. The entrypoint also chooses how the
81
+ // MCP endpoint runs: one server for every request, or one per request.
82
+ const serving = (requestScoped: boolean) =>
83
+ Layer.mergeAll(
84
+ api,
85
+ ActionMcp.layerHttp(implementations, {
86
+ name: "notes-service",
87
+ version: "1.0.0",
88
+ instructions: "Notes belong to the signed-in user. Read a note before updating it.",
89
+ authentication: Login,
90
+ requestScoped,
91
+ }),
92
+ health,
93
+ openApi,
94
+ ).pipe(Layer.provide(authenticate));
95
+
96
+ export const routes = serving(false);
97
+
98
+ export const requestScopedRoutes = serving(true);
85
99
  ```
86
100
 
87
- Files: [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and
88
- [quickstart/src/server.ts](../examples/quickstart/src/server.ts);
89
- [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) and
90
- [notes-service/src/server.ts](../examples/notes-service/src/server.ts).
91
-
92
- - Pass every surface the same list of implementations: the layer serves the binding's actions
93
- among them. Beside rpcs, one provider and one header decoding serve both
94
- ([ActionRpc.md](ActionRpc.md#serve-the-rpcs), "The same actions as JSON too";
95
- [kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts)).
96
- - A service a handler or an authorizer reads per request stays owed by the layer until router
97
- middleware around it, `HttpRouter.provideRequest` or the layer's own `middleware` provides it.
98
- Router middleware is the one that reads the request, such as a header:
99
- `HttpRouter.middleware<{ provides: Tenant }>()((route) => …)`, given as
100
- `Layer.provide(resolveTenant.layer)`
101
- ([kitchen-sink/src/workspaces/tenant.ts](../examples/kitchen-sink/src/workspaces/tenant.ts)).
102
- Without the type argument it runs and the layer still owes the service.
103
- `HttpRouter.provideRequest(layer)` builds its layer once, at startup, and gives every request
104
- that one value: never an identity or a tenant
105
- ([Action.md](Action.md#say-who-may-call-an-action)).
106
- - Give the server a `host`: `NodeHttpServer.layer(createServer, { port })` without one listens on
107
- every interface. The examples bind `127.0.0.1`.
108
- - Two listeners are two `HttpRouter.serve` calls, each building its routes on its own. What they
109
- share is provided outside both
110
- ([Action-in-process.md](Action-in-process.md#share-one-build-across-servers),
111
- [kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts)).
112
-
113
- Set a request body limit: a body has none unless the host provides one to `HttpRouter.serve`, as
114
- below. It covers every route of the server, MCP endpoints included, except a body a route reads
115
- as a stream. On Node (`NodeHttpServer`), a request over it gets no response, not a 413: its connection is
116
- closed. `HttpRouter.toWebHandler` ignores it, so limit bodies in the platform serving the handler.
117
-
118
- ```ts
119
- Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))),
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`.
103
+
104
+ The entrypoint serves the routes, with a request body limit; what that limit covers is on
105
+ `ActionHttp.layer`.
106
+
107
+ ```ts example=notes-service/src/server.ts
108
+ import { createServer } from "node:http";
109
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
110
+ import { ByteSize, Config, Effect, Layer } from "effect";
111
+ import { HttpRouter, HttpServerRequest } from "effect/http";
112
+ import { NoteStore } from "./notes/store.js";
113
+ import { routes } from "./routes.js";
114
+
115
+ // Entrypoint: `node --import tsx examples/notes-service/src/server.ts`.
116
+ // It chooses the store, reads the configuration and starts Node's HTTP server.
117
+ const server = Layer.unwrap(
118
+ Effect.gen(function* () {
119
+ // 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"));
121
+ const port = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
122
+
123
+ return HttpRouter.serve(routes).pipe(
124
+ Layer.provide(NodeHttpServer.layer(createServer, { host, port })),
125
+ // Request bodies have no size limit unless the host sets one. This covers every route that
126
+ // reads its body whole.
127
+ Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))),
128
+ // DEMO ONLY: notes live in this process's memory; a deployment provides a store that lasts.
129
+ Layer.provide(NoteStore.layerMemory),
130
+ );
131
+ }),
132
+ );
133
+
134
+ Layer.launch(server).pipe(NodeRuntime.runMain);
120
135
  ```
121
136
 
122
- An `ErrorReporter` the server runs with is also given, with severity `Info`, what a handler
123
- or `authorize` fails with and an input that does not decode, a declared 404, a 403 or a 400:
124
- Effect's `HttpApi` reports a route's failures, of which a refused credential is none. MCP and
125
- RPC report a defect alone. To keep a declared error out of a tracker, give its class
126
- `override readonly [ErrorReporter.ignore] = true`; a built-in error takes no mark, so drop
127
- `Info` there.
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`.
128
144
 
129
145
  ### Split a binding across layers, and add middleware
130
146
 
131
- Several layers may serve one binding, each with its own `actions` and `middleware`; serve each
132
- action from exactly one. Files:
147
+ Serve each action from exactly one layer. Files:
133
148
  [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`api`),
134
- [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts).
135
-
136
- | Where | Runs | Use it for |
137
- | ---------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- |
138
- | Global router middleware | before routing, on every route | a Host or Origin policy, CORS |
139
- | Router middleware provided to a `layer` call | around its routes, before authentication | a per-request service; a limit on unauthenticated requests |
140
- | The layer's `middleware` | after authentication, before decoding | a limit keyed by the caller, counting invalid input too |
141
- | The handler ([Action.md](Action.md#authorize-calls)) | after decoding and the authorizer | a limit that holds on every surface, MCP included |
142
-
143
- - A layer middleware requiring the identity needs a layer serving protected actions only. Serve
144
- the public actions from a second layer without it.
145
- - Router middleware answering with a binding error encodes and sends it itself,
146
- `HttpServerResponse.schemaJson(QuotaExceeded)(error, { status: 429 })`, at the error's
147
- `httpApiStatus`, or 422 without one: at any other status the client fails with an
148
- `HttpClientError`.
149
- - A request policy that must run before authentication and OAuth discovery:
150
- [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
149
+ [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts). Where a
150
+ check runs decides what it sees:
151
+
152
+ | Where | Runs | Use it for |
153
+ | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- |
154
+ | Global router middleware | before routing, on every route | a Host or Origin policy, CORS |
155
+ | 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 |
158
+
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).
151
162
 
152
163
  ### Publish the OpenAPI document
153
164
 
154
165
  Give `Http.api`, a plain `HttpApi`, to `OpenApi.fromApi`, `HttpApiSwagger.layer` or
155
- `HttpApiScalar.layer`, on routes of the host's own. Files:
156
- [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) (`openApi`),
157
- [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`documentation`).
158
-
159
- - The document's route is public unless you put middleware around it.
160
- - A header that middleware reads is not in the document: its operations list no `parameters`.
161
- Add it by rewriting the document,
162
- `OpenApi.fromApi(Http.api.annotate(OpenApi.Transform, (document) => …))`, or make the value an
163
- input.
164
- - The document and the clients list every action of the binding, served or not. An action no
165
- layer serves answers 404.
166
- - An OAuth protected resource is documented as a bearer scheme: Effect has no OAuth scheme
167
- ([Authentication.md](Authentication.md#publish-oauth-discovery)).
168
- - Serve the binding with `ActionHttp.layer`, never with the native `HttpApiBuilder`.
169
- - One document over several bindings, `HttpApi.make("app").addHttpApi(A.api).addHttpApi(B.api)`,
170
- works only when their prefixes differ and no action name repeats. Otherwise document each alone.
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`.
171
169
 
172
170
  ## Call the API from Effect
173
171
 
174
- `ActionHttp.client` yields a client with one method per action.
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`.
175
174
 
176
- ```ts
175
+ ```ts example=quickstart/src/client.ts
176
+ import { NodeRuntime } from "@effect/platform-node";
177
+ import { Console, Effect } from "effect";
178
+ import { FetchHttpClient } from "effect/http";
179
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
180
+ import { Http } from "./Api.js";
181
+
182
+ // Entrypoint: `node --import tsx examples/quickstart/src/client.ts`, with the server running.
183
+ // One typed method per action of the binding.
184
+ // DEMO ONLY: a deployed client reads its `baseUrl` from configuration (kitchen-sink/src/seed.ts).
177
185
  Effect.gen(function* () {
178
186
  const client = yield* ActionHttp.client(Http, { baseUrl: "http://127.0.0.1:3000" });
179
187
 
@@ -181,30 +189,11 @@ Effect.gen(function* () {
181
189
  }).pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain);
182
190
  ```
183
191
 
184
- Files: [quickstart/src/client.ts](../examples/quickstart/src/client.ts),
185
- [kitchen-sink/src/seed.ts](../examples/kitchen-sink/src/seed.ts) (`baseUrl` from `Config`,
186
- headers, errors by tag).
187
-
188
- - Outside a browser, set `baseUrl`. A token or any other header goes through `transformClient`:
189
- `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. A credential read or refreshed
190
- as each request is sent, which may fail, is
191
- `(client) => HttpClient.mapRequestEffect(client, authorize)`: each call then also fails with
192
- `authorize`'s own errors, as they are, and requires its services. `ActionMcp.client` takes the
193
- same transform, and `ActionCli.remote` the client itself as its `client`.
194
- - Leave the argument out when `{}` is a valid input. A given argument is the decoded input, so
195
- it states a defaulted field.
196
- - A declared error is a typed failure, as its decoded value: the action's own, the binding's,
197
- the verifier's, and the built-in `InvalidInput`, `Unauthenticated` and `Forbidden`. Handle
198
- each with `Effect.catchTag`.
199
- - Anything else is Effect's own: an `HttpClientError` when the server could not be reached or
200
- answered with a status or body the binding does not declare, such as a defect's empty 500; a
201
- `SchemaError` when the success does not decode.
202
- - The client interprets no status and retries nothing. A failed write may have happened; only a
203
- declared error says what the server did.
204
- - A handler calling another service through this client maps that service's `Unauthenticated`
205
- and `Forbidden` before failing with them
206
- ([Action.md](Action.md#fail-with-declared-and-built-in-errors)).
207
- - In a test, provide `Testing.layer(routes)` instead of a network client and leave `baseUrl` out
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`.
195
+
196
+ - In a test, provide `Testing.layer(routes)` instead of a network client
208
197
  ([Testing.md](Testing.md#test-routes-in-memory),
209
198
  [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts)).
210
199
 
@@ -215,21 +204,6 @@ with `Effect.runPromise`. Files:
215
204
  [notes-service/src/browser.ts](../examples/notes-service/src/browser.ts),
216
205
  [kitchen-sink/src/sdk.ts](../examples/kitchen-sink/src/sdk.ts) (a `baseUrl`, two headers).
217
206
 
218
- - A page on another origin passes `baseUrl`, and the server must allow that origin with
219
- `HttpRouter.cors`
220
- ([kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts), `cors`).
221
- - A `transformClient` requiring a service, such as the session's token, makes each call require
222
- it: provide it on the call before `Effect.runPromise`.
223
- - An unhandled declared error rejects the promise as its decoded value, so
224
- `error instanceof Action.Unauthenticated` works in a `catch`. Handle an expected one by its tag
225
- before `Effect.runPromise`.
226
- - A page on the API's own origin sends its cookies already. From another origin, pass `fetch`:
227
- `(input, init) => fetch(input, { ...init, credentials: "include" })`, and allow credentials in
228
- the server's CORS, `HttpRouter.cors({ allowedOrigins, credentials: true })`. A `fetch` of your
229
- own keeps `init.signal`, which aborts the request when its call is interrupted: a deadline is
230
- `Effect.timeout` on the call.
231
- - A route answers a request not typed as JSON with 415, so what another site's page can send
232
- without a CORS preflight, such as a form post carrying the user's cookie, never reaches a
233
- handler; the server's CORS policy decides the rest.
234
- - The page imports the binding from a module with no server code
235
- ([setup.md](setup.md#bundle-for-a-browser)).
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`.
209
+ - Bundling the page: [setup.md](setup.md#bundle-for-a-browser).
@@ -1,36 +1,16 @@
1
1
  # ActionMcp failure modes
2
2
 
3
- Search for a fragment of the message. The tasks are in [ActionMcp.md](ActionMcp.md).
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionMcp.md](ActionMcp.md).
4
4
 
5
- - `MCP tool input must be one object with keys, such as a struct: <name>, ...`: those actions' input is a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Wrap it in a field, `input: { notification: Schema.Union([Email, Sms]) }`, omit `input` for no arguments, or leave the action out of `actions`.
6
- - `MCP media must be the success or an array of it, or a top-level field of a struct success, one, optional or a required array: <name>, ...`: those actions hold an `Action.Image` no tool can send as a block. Make it a top-level field of a `Schema.Struct` success, give an optional array as a required one, empty when absent, or leave the action out of `actions`.
7
- - `Duplicate MCP tool: <name>`: two implementations serve actions of one name. Split the endpoint or rename one. `actions` tells apart only separate contracts sharing a name; of two implementations of one contract, pass one.
8
- - `Duplicate MCP tool: <name>, claimed by an action and a native feature`, a defect when the endpoint builds: a native tool among `features` has an action's name. Rename it, or leave the action out of `actions`.
9
- - `Listed in actions, but no implementation holds it: <names>`: those actions are not among the implementations', by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding it, or list the contract it implements.
10
- - `Property 'authentication' is missing` on `layerHttp`'s options: the endpoint serves a protected action and names no descriptor. `Type 'Other' is not assignable to type 'CurrentUser'` on `authentication`: the descriptor is another identity's. Give the protected actions' descriptor.
11
- - `Authentication "<name>": the binding's descriptor is not its provider's`, a defect when `layerHttp` builds: give the endpoint the descriptor its provider was built from ([Authentication-failures.md](Authentication-failures.md)).
12
- - `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in an MCP endpoint's type: a handler or an authorizer yields a request service no middleware around the endpoint provides. Provide it with router middleware; never an identity at startup. `X` the identity, on an endpoint serving public tools: a public handler reads it, which no public tool gets, signed in or not. Make that action protected.
5
+ - `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in an MCP endpoint's type: a handler or an authorizer yields a request service no middleware around the endpoint provides. Provide it with router middleware. `X` the identity, on an endpoint serving public tools: a public handler reads it, which no public tool gets, signed in or not. Make that action protected.
13
6
  - `Type 'CurrentUser' is not assignable to type 'never'` where the server is launched, with `CurrentUser` among an MCP endpoint's startup requirements, not its `Request<"Requires", ...>`: a native feature reads a request service, which features never receive. Serve that content as an action.
14
7
  - `Type 'CurrentUser' is not assignable to type 'never'` at a stdio program's `runMain`: it serves a protected tool, whose caller the host supplies. Provide the identity around `runStdio`.
15
- - A native resource, prompt or toolkit layer is never served (`resources/list` is empty, `prompts/list` is not found, `tools/list` lists only the actions): an endpoint serves only the native layers passed as its `features`; one merged beside it builds without error. Pass it as `features`.
16
- - A prompt or resource demands credentials: on an endpoint naming `authentication`, every prompt and resource request needs one. Serve them on an endpoint without protected tools.
17
- - A public tool's call answers 401: a public tool needs no credential, but one a request presents is still verified, and this one failed. Send a valid one, or none.
18
- - A protected tool's call answers 401 to a signed-in client after a while: its token expired. The client refreshes it, or signs in again, on the challenge's `invalid_token`.
19
- - An MCP client gets an `isError` refusal, not the 401 or 403 it re-authorizes on: the handler sent a notification first, which starts the response, so its refusal is a tool result. Refuse in `authorize`.
20
- - A client that opens with `initialize` gets 401 where it lists the tools or calls a public one, on an endpoint that also serves protected tools: a session's every request authenticates. Sign the client in, or serve the public tools on an endpoint of their own.
21
8
  - A client that opens with `initialize`, such as Codex, gets 400 with JSON-RPC error `-32020`, `MCP-Protocol-Version header is required`, or 401 signed out where the endpoint serves a protected tool, and a `DELETE` gets 405: the endpoint is `requestScoped`, which speaks MCP 2026-07-28 alone and keeps no session. Connect that client to an endpoint without it, on a host that keeps a process running.
22
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`.
23
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.
24
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).
25
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`.
26
13
  - 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.
27
- - A client asking for 2025-03-26 or 2024-11-05 is offered 2025-11-25, and disconnects if it cannot speak it: HTTP and stdio serve 2026-07-28, 2025-11-25 and 2025-06-18, a `requestScoped` endpoint 2026-07-28 alone. Update the client.
28
14
  - 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`.
29
- - A client reports a broken transport from a stdio subprocess: something wrote to stdout. The global `console.log` did, or a layer provided around `runStdio` did, through the default logger, or through `Console` or a logger such as `Logger.consoleJson`, which `logToStderr` does not move. Remove the write, apply `ActionCli.logToStderr` last, or log JSON with `Logger.withConsoleError(Logger.formatJson)`.
30
- - A request carrying an `Origin` gets an empty 403: the origin is not in `allowedOrigins`. Add the exact origin, if the deployment trusts it.
31
- - A disallowed `Origin` gets 401, not 403: the endpoint's authentication, which runs before its Origin check, refused the request.
32
- - Browser calls fail despite an allowed origin, the preflight answering 405, or 401 on an endpoint with `authentication`, which every method but `POST` meets first: `allowedOrigins` is no CORS. Add `HttpRouter.cors` ([browser clients](ActionMcp.md#allow-browser-clients)).
33
15
  - `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.
34
- - `MCP tools/call "<name>" answered 404`: the endpoint is not at `/mcp`, the default. Pass `ActionMcp.client` its `url`.
35
- - `MCP tools/call "<name>" returned an error: Invalid parameters for tool ...`: the endpoint serves another contract under that name. Pass `ActionMcp.client` the served contract value.
36
16
  - `UrlError` from `HttpClientRequest.toWebResult` or `toWeb` of an `ActionMcp.request`, or an `HttpClientError` whose reason is `InvalidUrlError` from a client method or a sent `ActionMcp.request`: the `url` is relative, such as the default `/mcp`, which only the `HttpClient` of `Testing.layer` resolves. Pass an absolute one.