@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
package/docs/ActionMcp.md CHANGED
@@ -5,82 +5,42 @@ after it. `layerHttp` mounts a Streamable HTTP endpoint, for clients that connec
5
5
  `runStdio` is the whole program of a subprocess a local agent starts. For a model your own code
6
6
  calls in process, use [ActionToolkit.md](ActionToolkit.md).
7
7
 
8
- Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
9
- When something fails, search [ActionMcp-failures.md](ActionMcp-failures.md). Reading order:
10
- [README.md](README.md).
11
-
12
- ## API
13
-
14
- Import `@gjermundgaraba/effect-actions/ActionMcp`.
15
-
16
- | API | Purpose |
17
- | ------------------------------------- | ----------------------------------------------------------- |
18
- | `layerHttp(implementations, options)` | A Layer serving the actions as tools at one HTTP endpoint. |
19
- | `runStdio(implementations, options)` | An Effect serving them on standard I/O until stdin closes. |
20
- | `Options` | Type of `runStdio`'s options. |
21
- | `LayerHttpOptions` | Type of `layerHttp`'s options: `Options` and HTTP's own. |
22
- | `client(actions, options?)` | An Effect of a client calling each action's tool. |
23
- | `request(method, params?, options?)` | One MCP request, as the native `HttpClientRequest` to send. |
24
- | `McpCallError` | A client method's undecodable answer, by tagged `reason`. |
25
-
26
- `implementations` is one implementation or a list.
27
-
28
- | Option | Meaning |
29
- | ------------------------------------ | ----------------------------------------------------------- |
30
- | `name`, `version` | Required server information. |
31
- | `description`, `websiteUrl`, `icons` | Optional server information. |
32
- | `instructions` | Optional text for the model about the server as a whole. |
33
- | `extensions` | Optional native capability extensions. |
34
- | `actions` | The implementations' actions that are tools; default all. |
35
- | `features` | One Layer of native resources, prompts and tools to serve. |
36
- | `path` | HTTP only: the endpoint's path; defaults to `/mcp`. |
37
- | `allowedOrigins` | HTTP only: the exact origins allowed; none by default. |
38
- | `allowSubscriptions` | HTTP only: 2026-07-28 subscriptions; default if `features`. |
39
- | `requestScoped` | HTTP only: `true` runs a server per request. |
40
- | `authentication` | HTTP only: the protected tools' authentication descriptor. |
41
-
42
- All but `actions`, `features`, `authentication` and `requestScoped` are Effect's own `McpServer`
43
- options. Its `protocols` and `allowSessionTermination` are not options.
44
-
45
- `client` takes `url`, default `/mcp`, `transformClient`, and the endpoint's `authentication`, so an
46
- error its verifier declares decodes; `request` takes `url` and `headers`.
47
- Types: `Client<Actions, D, E, R>`, `ClientOptions`, `RequestOptions`, `Params`.
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.
48
12
 
49
13
  ## Serve tools over HTTP
50
14
 
51
15
  Merge `layerHttp` into the routes, beside any other surface, and serve them with
52
16
  `HttpRouter.serve`:
53
17
 
54
- ```ts
18
+ ```ts example=quickstart/src/routes.ts
19
+ import { Layer } from "effect";
20
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
21
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
22
+ import { Http } from "./Api.js";
23
+ import { greeting } from "./greeting/handlers.js";
24
+
25
+ // One implementation, two surfaces: a JSON route and an MCP tool at `/mcp`.
55
26
  export const routes = Layer.mergeAll(
56
27
  ActionHttp.layer(Http, greeting),
57
28
  ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" }),
58
29
  );
59
30
  ```
60
31
 
61
- See [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and `server.ts` beside it.
62
- Surfaces in one set of routes run each builder once, so a note written over HTTP is read as a
63
- tool: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). For two servers,
64
- see [Action-in-process.md](Action-in-process.md#share-one-build-across-servers).
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).
65
35
 
66
- - The endpoint speaks MCP 2026-07-28, 2025-11-25 and 2025-06-18, as the client negotiates.
67
- Claude Code 2.1 and Codex CLI 0.162 connect (October 2026):
36
+ - Claude Code 2.1 and Codex CLI 0.162 connect:
68
37
  `claude mcp add --transport http <name> <url>/mcp --header "Authorization: Bearer <token>"`,
69
38
  `codex mcp add <name> --url <url>/mcp --bearer-token-env-var <VAR>`;
70
39
  [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has stdio and Claude Desktop.
71
- - **A client of a 2025 revision opens a session** with `initialize`; a 2026-07-28 request stands
72
- alone. A session holds what its client sent there and no identity: each request authenticates,
73
- as its own caller. It lasts until its client ends it with a `DELETE`, answered 204, or the process stops, as
74
- Effect expires none, so limit request bodies, which bounds what one holds. Where an endpoint
75
- serves a protected tool, only a signed-in caller opens one, and that client signs in for every
76
- request, a public tool's call included. An endpoint of public tools alone lets anyone open them.
77
- - **On a serverless host**, such as Cloudflare Workers, pass `requestScoped: true`: a server per
78
- request, speaking 2026-07-28 alone with no session. Connect a client that opens with
79
- `initialize` to a long-lived endpoint: [notes-service/src/worker.ts](../examples/notes-service/src/worker.ts).
80
- - An endpoint without `authentication` answers whoever reaches its port. Bind it to loopback or to
81
- a network only trusted clients are on, and limit request bodies
82
- ([ActionHttp.md](ActionHttp.md#serve-the-routes)):
40
+ - Where an endpoint without `authentication` listens, and its body limit:
83
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).
84
44
  - The endpoint takes no layer middleware. A limit that must hold for tools goes in the handlers:
85
45
  [kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts).
86
46
 
@@ -88,144 +48,99 @@ see [Action-in-process.md](Action-in-process.md#share-one-build-across-servers).
88
48
 
89
49
  Provide `Stdio` and the startup services, then run it:
90
50
 
91
- ```ts
51
+ ```ts example=notes-service/src/stdio.ts
52
+ import { NodeRuntime, NodeStdio } from "@effect/platform-node";
53
+ import { Effect } from "effect";
54
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
55
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
56
+ import { identity } from "./identity/handlers.js";
57
+ import { CurrentUser } from "./identity/Identity.js";
58
+ import { users } from "./identity/users.js";
59
+ import { notes } from "./notes/handlers.js";
60
+ import { NoteStore } from "./notes/store.js";
61
+ import { status } from "./status/handlers.js";
62
+
63
+ // Entrypoint: an MCP server a local agent starts as a subprocess, such as
64
+ // `claude mcp add notes -- "$PWD/node_modules/.bin/tsx" "$PWD/examples/notes-service/src/stdio.ts"`
65
+ // from the repository root. The host starts the process in a directory it chooses, so `tsx`
66
+ // and the file are named by full path.
67
+ // The same implementations as the HTTP server, and the same `authorize`. No token arrives
68
+ // over stdio, so this host names the caller itself. It is a process of its own with services
69
+ // of its own. DEMO ONLY: this in-memory store holds its notes, not the running server's. To
70
+ // share them, both provide a store they both reach, such as a database.
92
71
  ActionMcp.runStdio([status, notes, identity], { name: "notes-service", version: "1.0.0" }).pipe(
93
72
  // DEMO ONLY: a real host reads who started it, from its own login or configuration.
94
73
  Effect.provideService(CurrentUser, users.ada),
95
74
  Effect.provide([NodeStdio.layer, NoteStore.layerMemory]),
75
+ // stdout carries the protocol alone, so logs and failures go to stderr.
96
76
  ActionCli.logToStderr,
97
77
  NodeRuntime.runMain,
98
78
  );
99
79
  ```
100
80
 
101
- See [notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts), and
102
- [mcp-tools/src/stdio.ts](../examples/mcp-tools/src/stdio.ts) for public tools.
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).
103
84
 
104
- To try a server by hand, send one request as `request` builds it, its 2026-07-28 metadata in
105
- `_meta`: [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) has the `curl`, with
106
- HTTP's five headers, and [notes-service](../examples/notes-service/README.md#run-it) the stdio command.
107
-
108
- - **stdout carries the protocol alone.** `runStdio` sends its own program's Effect logs and
109
- `Console` output to stderr. For the layers provided around it, apply `ActionCli.logToStderr`
110
- last, before `runMain`: it moves their default logger and reports a startup failure there too.
111
- Nothing moves the global `console.log`: keep it off stdout.
112
- - **A subprocess has its own services.** It runs the handlers itself, so it shares a server's
113
- data only through a store both reach, never one in memory. A host that should see what the
114
- server holds connects to its HTTP endpoint.
115
- - **No token arrives, so the host names the caller.** stdio takes no `authentication`: provide the
116
- protected tools' identity around `runStdio`, whose one caller is whoever started it. Each
117
- implementation's `authorize` still runs, so give that caller what it checks. Tools served
118
- behind a token over HTTP keep their one protected contract here: never a public copy.
119
- - **Test the tools without a process**: [Testing.md](Testing.md#test-mcp-tools).
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.
120
89
 
121
90
  ## Describe tools to a model
122
91
 
123
92
  A tool is its contract ([Action.md](Action.md#define-a-contract)): a model knows only the action's
124
- name, its `description`, and its input schema with each field's `description`. `readOnly` is the
125
- tool's read-only hint, and the contract's `mcp` option holds its `title`, its other hints and its
126
- `_meta`. The server's `instructions` say what the tools are for together. See
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
127
96
  [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts) and
128
97
  [mcp-tools/src/instructions.ts](../examples/mcp-tools/src/instructions.ts).
129
98
 
130
- - MCP takes a write as destructive unless it states `destructiveHint: false`: one that only adds.
131
- - A tool's input is one object with keys. Omit `input` for a tool without arguments, and wrap a
132
- union, an array or a scalar in a field.
133
- - An argument the schema refuses, an undeclared one included, never reaches the handler: it
134
- is an `isError` result, text `Invalid parameters for tool '<name>': …`, not `InvalidInput`,
135
- and to 2025-06-18 a JSON-RPC error. So constrain in the schema
136
- what a model may send: an id written into an upstream URL is not made safe by
137
- `encodeURIComponent`, which leaves `..` alone.
138
- - A `Schema.makeFilter` check adds nothing to the tool's listed input schema, and
139
- `Schema.isPattern` its `pattern` only for a RegExp with the `u` flag. Say such a rule in the
140
- field's `description`, annotated before the check: after a custom one it is lost too.
141
- - A declared error reaches the model as an `isError` result whose text is the error's JSON. Write
142
- its `message` for the model, which acts on it:
143
- [mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts). Every other case:
144
- [guarantees.md](guarantees.md#wire-behavior).
145
- - `actions` lists the tools, and only listed actions must suit MCP:
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:
146
105
  [kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts). Leave out an action a
147
106
  model should not call, or one that takes a secret ([Action.md](Action.md#define-a-contract)).
148
107
 
149
108
  ## Return images and other media
150
109
 
151
- Give the success an `Action.Image` field, the one media kind, and return its bytes and MIME type
152
- from the handler:
153
-
154
- ```ts
155
- success: { printer: Printer, image: Action.Image },
156
- ```
157
-
158
- The tool sends the field as an image block, after the JSON of the rest. Every other surface sends
159
- it as JSON, the bytes in base64. See
160
- [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts); `client`
161
- decodes it back.
162
-
163
- - An image is a top-level field of a struct success, bare, optional or a required array, or the
164
- whole success or an array of it. Anywhere else in a served action, as in a `Schema.NullOr`, a
165
- `Schema.Class` success or an input, `layerHttp` and `runStdio` throw. So an action taking an
166
- image as input is no tool: leave it out of `actions`
167
- ([kitchen-sink/src/attachments/Attachments.ts](../examples/kitchen-sink/src/attachments/Attachments.ts)).
168
- - A media field's own `description` reaches no model. Describe the image in the action's
169
- `description`.
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).
170
115
 
171
116
  ## Protect tools
172
117
 
173
- Name the protected actions' authentication descriptor and provide its provider to the routes:
174
-
175
- ```ts
176
- const mcp = ActionMcp.layerHttp(implementations, {
177
- name: "notes-service",
178
- version: "1.0.0",
179
- authentication: Login,
180
- });
181
-
182
- export const routes = Layer.mergeAll(api, mcp, health, openApi).pipe(Layer.provide(authenticate));
183
- ```
184
-
185
- See [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). Write the
186
- [descriptor](Authentication.md#declare-how-callers-sign-in) and its
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
187
121
  [provider](Authentication.md#verify-credentials) once for every surface, and
188
122
  [publish discovery](Authentication.md#publish-oauth-discovery) so an MCP client finds where to
189
123
  sign in. What a signed-in caller may do is each implementation's
190
124
  [`authorize`](Action.md#authorize-calls), which runs on every call of a protected tool.
191
125
 
192
- - **One endpoint or two.** An endpoint of protected tools alone authenticates every request,
193
- the listing included. On a mixed endpoint, serving public and protected tools, a 2026-07-28
194
- request lists every tool, protected ones included, and calls a public one signed out, and a
195
- session's every request signs in ([Serve tools over HTTP](#serve-tools-over-http)). A
196
- protected tool's call answers the 401 a client signs in on. To keep protected tools unlisted
197
- to signed-out callers, or public ones open to a session's client, serve the public tools on an
198
- endpoint of their own: a second `layerHttp` on another `path`.
199
- - No listing is filtered by caller: `authorize` refuses the call, not the sight of the tool.
200
- - On a Bearer endpoint, a protected tool's step-up refusal answers the request, not a tool
201
- result ([Authentication.md](Authentication.md#step-up-for-a-scope),
202
- [guarantees.md](guarantees.md#wire-behavior)). Once a handler has sent a notification the
203
- response has started, and its refusal is a tool result: refuse in `authorize`.
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`.
204
128
  - Test as a signed-in caller ([Testing.md](Testing.md#test-as-a-signed-in-caller)):
205
129
  [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts).
206
130
 
207
131
  ## Allow browser clients
208
132
 
209
- A browser MCP client sends its page's `Origin`, and needs two things.
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.
210
138
 
211
- - **`allowedOrigins`.** A request carrying an `Origin` is refused with 403 unless that exact
212
- origin is listed, so a page a user visits cannot call the tools; one without, as other
213
- clients send, passes. Authentication runs first, so the verifier sees a token from any origin:
214
- a request that must authenticate, or sends a token, gets 401 without a valid one. Read the
215
- list from config: [mcp-tools/src/routes.ts](../examples/mcp-tools/src/routes.ts).
216
- - **CORS.** `allowedOrigins` only refuses: it answers no preflight and adds no header. Add
217
- `HttpRouter.cors`, global to the router, only where an origin is configured: given an empty
218
- list, it answers `Access-Control-Allow-Origin: *`, and given one origin it names that origin
219
- to every request, an unlisted page's included, which `allowedOrigins` still refuses. Allow the methods `POST` and
220
- `DELETE`, the endpoint's only ones, and the headers `Content-Type`, `Authorization`,
221
- `MCP-Protocol-Version`, `MCP-Method`, `MCP-Name` and `Mcp-Session-Id`; expose
222
- `WWW-Authenticate`, `MCP-Protocol-Version` and `Mcp-Session-Id`: the same `routes.ts`, and
223
- [kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts) with
224
- authentication.
225
-
226
- A `Host` allowlist, for a request that sends no `Origin`, or an Origin check that must run first
227
- is a request policy:
228
- [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
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).
229
144
 
230
145
  ## Add native prompts and resources
231
146
 
@@ -233,15 +148,9 @@ Merge Effect's own `McpServer.resource`, `McpServer.prompt` and `McpServer.toolk
233
148
  one and pass it as `features`:
234
149
  [kitchen-sink/src/issues/guide.ts](../examples/kitchen-sink/src/issues/guide.ts), given in
235
150
  [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and
236
- [kitchen-sink/src/stdio.ts](../examples/kitchen-sink/src/stdio.ts). Provide a service they need
237
- around the endpoint, never to the `features` layer itself: there it is built again for the
238
- features alone, so a resource reads another store than the tools write.
239
-
240
- - No `authorize` runs for them, and they run with the services they were built with, never a
241
- request's, so they cannot read the caller. Content that depends on the caller is an action.
242
- - On an endpoint naming `authentication`, every prompt and resource request needs a credential,
243
- listings included. To offer them to signed-out callers, serve them on an endpoint without
244
- protected tools.
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`.
245
154
 
246
155
  ## Send notifications from a handler
247
156
 
@@ -3,19 +3,10 @@
3
3
  What you see when something is wrong, and the fix. Search for a fragment of the message.
4
4
  The tasks are in [ActionRpc.md](ActionRpc.md).
5
5
 
6
- - `No action of these implementations is in this RPC binding: <names>` thrown by `layer`: none of the implementations' actions was passed to this binding's `make`. Implement the contract value the binding received; `(another contract)` marks an equal-looking copy.
7
- - `Listed in actions, but the binding does not hold it: <names>`, or `but no implementation holds it`, thrown by `layer`: an action in `layer`'s `actions` is not the binding's or the implementations', by identity.
8
- - `Duplicate served action: <name>` thrown by `layer`: one `layer` call received two implementations of one bound action. Pass one.
9
- - `ActionRpc binding: error _tag "<tag>" is built in, and declared on every surface` thrown by `make`: the binding's `error` holds a built-in error, or one reusing a built-in tag. Drop the built-in one; rename your own.
10
- - `Protected action '<name>' requires its matching authentication descriptor` thrown by `make` or `layer` in plain JavaScript, a `No overload matches this call` at `make` in TypeScript: the binding holds a protected action and names no descriptor, or one of another identity. Give `{ authentication: Login }`.
11
- - `ActionRpc serves a protocol speaking JSON: ...`, a defect when `layer` builds: the host provides another serialization. Provide a JSON one, on the client too.
12
6
  - A type error on `middleware` naming `"Layer middleware fails only with the binding's errors and needs no client"`: a middleware declares an error neither the binding nor the built-ins hold, or is `requiredForClient`. Add the error to the binding's `error`, or check in the handler, declaring the error on its actions.
13
- - `Provider<CurrentMember, "live-rpc.Login">` among the requirements of `ActionRpc.layer`: the layer serves a protected action, and its authentication provider is not provided. Provide it, `Layer.provide(authenticate)`.
14
- - `Type 'HttpRouter' is not assignable to type 'never'` where the server is launched: an rpc protocol adds its route to the router, so it belongs inside what `HttpRouter.serve` serves, and here it was provided around `HttpRouter.serve`. Provide the protocol to the rpc layer.
7
+ - `Type 'HttpRouter' is not assignable to type 'never'` where the server is launched: an rpc protocol was provided around `HttpRouter.serve`, whose router it adds its route to. Provide the protocol to the rpc layer.
15
8
  - `HttpRouter` owed by an `ActionRpc.layer` over stdio or a socket server: its authentication provider publishes a `protectedResource`, on a router those protocols lack. Give the provider serving that protocol no `protectedResource`.
16
9
  - `Request<"Requires", X>` owed by an `ActionRpc.layer` over stdio or a socket server though no handler yields `X`: the verifier yields `X`, which only a router-mounted protocol's route provides. Read `X` in the verifier's build, or serve the protected actions over HTTP or a WebSocket.
17
- - `Request<"Requires", X>` in the type of `ActionRpc.layer`, or `X` not assignable to `never` where the server is launched: a handler or the authorizer yields `X`, which nothing provides per request. A startup layer of `X` does not count, nor a middleware in a list not written as a tuple, nor one listed before a middleware requiring `X`: the first listed is innermost. List a middleware providing `X`, or provide `X` with router middleware around the layer. Where `X` is the identity, a middleware requires it on a layer serving a public action: bind the protected actions alone, or read the identity with `Effect.serviceOption`.
18
- - `Unauthenticated` `A bearer token is required.` from a browser's WebSocket though the page has a token: a browser WebSocket sets no header on its upgrade, so a token meant for the upgrade is never sent. Send the token on each message, with `RpcClient.withHeaders`.
19
- - A call dies with `Unknown request tag: <name>`: no layer serves the action, for which the client has a method all the same, or two `ActionRpc.layer`s were given one protocol, and only one receives its messages. Serve every rpc of a protocol from one layer; a second layer needs a protocol of its own, on another path.
20
- - A call dies with `Internal server error`: a defect, an undeclared failure, a middleware that throws, or a success or a declared failure that does not encode. The cause is in the server's logs and error reporters.
10
+ - `Request<"Requires", X>` in the type of `ActionRpc.layer`, or `X` not assignable to `never` where the server is launched: a handler, the authorizer or a middleware yields `X`, which nothing provides per request, and a startup layer of `X` does not count. List a middleware providing `X`, by the rules on `ActionRpc.LayerOptions.middleware`, or provide `X` with router middleware around the layer.
11
+ - A call dies with `Unknown request tag: <name>`: no layer serves the action, for which the client has a method all the same, or a second `ActionRpc.layer` was given the same protocol. Serve every rpc of a protocol from one layer, or give the second a protocol on another path.
21
12
  - `RpcClientError` from every call, Effect's own error: the client could not reach the server. Check the client's protocol, URL and path against the server's.