@gjermundgaraba/effect-actions 0.10.0 → 0.12.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 (214) hide show
  1. package/README.md +103 -43
  2. package/dist/Action.d.mts +31 -118
  3. package/dist/Action.mjs +15 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +25 -31
  9. package/dist/ActionMcp.mjs +140 -74
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +5 -8
  13. package/dist/ActionToolkit.mjs +3 -3
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +32 -25
  17. package/dist/Testing.mjs +45 -30
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +17 -0
  38. package/dist/mcp/protocol.mjs +149 -0
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +206 -424
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -292
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -281
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +229 -193
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +178 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +100 -117
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +142 -392
  59. package/docs/CONTEXT.md +39 -23
  60. package/docs/README.md +71 -35
  61. package/docs/Testing-failures.md +20 -0
  62. package/docs/Testing.md +186 -151
  63. package/docs/guarantees.md +35 -95
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +88 -87
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +317 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +75 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +136 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +119 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +30 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +108 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +89 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +91 -0
  183. package/examples/notes-service/src/notes/handlers.ts +33 -0
  184. package/examples/notes-service/src/notes/store.ts +73 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +134 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +32 -14
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -11
  211. package/dist/internal/mcp.mjs +0 -52
  212. package/dist/internal/refusal.mjs +0 -67
  213. package/dist/internal/schema-errors.mjs +0 -20
  214. package/dist/internal/tools.mjs +0 -44
package/docs/ActionMcp.md CHANGED
@@ -1,223 +1,259 @@
1
1
  # ActionMcp
2
2
 
3
- MCP tools from implementations, on Effect's native `McpServer`. One `Tool` per served action,
4
- named after it. Two transports: a Streamable HTTP endpoint mounted on the router, or newline-delimited
5
- JSON-RPC on standard I/O for a subprocess. HTTP speaks MCP 2026-07-28 only; stdio also speaks
6
- 2025-11-25 and 2025-06-18, as the host negotiates.
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).
7
+
8
+ Read this page alone, in one tool call: pages read together are cut. When something fails,
9
+ search [ActionMcp-failures.md](ActionMcp-failures.md). Reading order: [README.md](README.md).
7
10
 
8
11
  ## API
9
12
 
10
13
  Import `@gjermundgaraba/effect-actions/ActionMcp`.
11
14
 
12
- | API | Purpose |
13
- | ------------------------------------- | -------------------------------------------------------------------------------------------------- |
14
- | `layerHttp(implementations, options)` | Serve implementations at a Streamable HTTP endpoint. |
15
- | `runStdio(implementations, options)` | Serve implementations as a subprocess's program on standard I/O; succeeds when the host closes it. |
16
-
17
- The options, exported as `LayerHttpOptions` and `Options` (the server's, which `LayerHttpOptions` extends), are the native
18
- `McpServer.layerHttp` / `McpServer.layerStdio` options, except `protocols`, plus `actions` and `features`, and for `layerHttp`, `authentication`. The native ones pass through unchanged; `path` gains a default. Each implementation brings its
19
- authorization ([Action.md](Action.md#implementations)); an endpoint serving protected actions
20
- names their authentication descriptor and requires its provider
21
- ([Authentication.md](Authentication.md)).
22
-
23
- | Option | Meaning |
24
- | ------------------------------------ | ------------------------------------------------------------------------------ |
25
- | `name`, `version` | Required native server information. |
26
- | `description`, `websiteUrl`, `icons` | Optional native server information, sent to clients with `name` and `version`. |
27
- | `instructions` | Optional native server instructions. |
28
- | `extensions` | Optional native server capability extensions. |
29
- | `path` | HTTP endpoint path; HTTP only, defaults to `/mcp`. |
30
- | `allowedOrigins` | Optional exact Origin allowlist; HTTP only, not CORS configuration. |
31
- | `authentication` | HTTP only: the protected tools' descriptor, required where one is served. |
32
- | `actions` | Optional actions that are tools, among the implementations'; defaults to all. |
33
- | `features` | Optional native resources, prompts and tools served beside the actions' tools. |
34
-
35
- `implementations` is one implementation or a list. Every tool declares its action's errors plus the
36
- built-in `InvalidInput`, `Unauthenticated` and `Forbidden`. `layerHttp`'s layer and `runStdio`'s
37
- program retain the build failures and requirements of their builders, plus native
38
- `IllegalArgumentError`. HTTP needs the router, the descriptor's provider where it serves a
39
- protected action, and wraps handler and authorizer services as request requirements until middleware provided around it provides them, the
40
- identity excepted, which the provider supplies. stdio needs `Stdio` and the caller's request
41
- services, the identity of its protected tools included. Native `McpRequestContext`
42
- is supplied by the server, not required of the host. The server also fails as its `features` do
43
- and needs what they need, except the registry, `McpServer.McpServer`, which the endpoint provides,
44
- such as to a feature registering with `McpServer.registerResource`.
45
-
46
- ## Canonical
47
-
48
- One endpoint serves the public and the protected tools: discovery and `status` answer anyone,
49
- and every other request authenticates before the endpoint decodes it.
50
-
51
- ```ts example=mcp.ts
52
- import { Layer } from "effect";
53
- import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
54
- import { authenticate } from "./authentication.js";
55
- import { Login } from "./binding.js";
56
- import { double, status, userActions } from "./handlers.js";
57
-
58
- const allowedOrigins = ["http://localhost:3000", "http://127.0.0.1:3000"];
59
-
60
- // One URL: discovery and `status` answer anyone; a protected tool's call authenticates
61
- // before its arguments are decoded, with the 401 an MCP client signs in on.
62
- export const layer = ActionMcp.layerHttp([status, userActions, double], {
63
- name: "effect-actions",
64
- version: "0.0.0",
65
- allowedOrigins,
66
- authentication: Login,
67
- }).pipe(Layer.provide(authenticate));
68
- ```
69
-
70
- ### Cross-origin browsers
15
+ | API | Purpose |
16
+ | ------------------------------------- | ------------------------------------------------------------ |
17
+ | `layerHttp(implementations, options)` | A Layer serving the actions as tools at one HTTP endpoint. |
18
+ | `runStdio(implementations, options)` | An Effect serving them on standard I/O until stdin closes. |
19
+ | `Options` | Type of `runStdio`'s options. |
20
+ | `LayerHttpOptions` | Type of `layerHttp`'s options: `Options` and three for HTTP. |
71
21
 
72
- `allowedOrigins` alone does not configure CORS. For a stateless browser endpoint, mount
73
- native router CORS outside the route middleware. This CORS layer is global to the router;
74
- choose its policy for every route it covers.
22
+ `implementations` is one implementation or a list.
75
23
 
76
- ```ts example=mcp-browser.ts
77
- import { Effect, Layer } from "effect";
78
- import { HttpRouter } from "effect/http";
79
- import * as Action from "@gjermundgaraba/effect-actions/Action";
80
- import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
81
- import { Greet } from "./quickstart.js";
24
+ | Option | Meaning |
25
+ | ------------------------------------ | ---------------------------------------------------------- |
26
+ | `name`, `version` | Required server information. |
27
+ | `description`, `websiteUrl`, `icons` | Optional server information. |
28
+ | `instructions` | Optional text for the model about the server as a whole. |
29
+ | `extensions` | Optional native capability extensions. |
30
+ | `actions` | The implementations' actions that are tools; default all. |
31
+ | `features` | One Layer of native resources, prompts and tools to serve. |
32
+ | `path` | HTTP only: the endpoint's path; defaults to `/mcp`. |
33
+ | `allowedOrigins` | HTTP only: the exact origins allowed; none by default. |
34
+ | `authentication` | HTTP only: the protected tools' authentication descriptor. |
82
35
 
83
- const allowedOrigins = ["https://ui.example.com"];
36
+ All but `actions`, `features` and `authentication` are Effect's own `McpServer` options, passed
37
+ through. Its `protocols` and `allowSessionTermination` are not options.
84
38
 
85
- const actions = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
39
+ ## Serve tools over HTTP
86
40
 
87
- const mcp = ActionMcp.layerHttp(actions, { name: "greetings", version: "1.0.0", allowedOrigins });
41
+ Merge `layerHttp` into the routes, beside any other surface, and serve them with
42
+ `HttpRouter.serve`:
88
43
 
89
- // Global router CORS handles preflight outside route-level authentication.
90
- // This example is public; an endpoint serving protected actions takes their `authentication`.
44
+ ```ts
91
45
  export const routes = Layer.mergeAll(
92
- mcp,
93
- HttpRouter.cors({
94
- allowedOrigins,
95
- allowedMethods: ["POST"],
96
- allowedHeaders: [
97
- "Content-Type",
98
- "Authorization",
99
- "MCP-Protocol-Version",
100
- "MCP-Method",
101
- "MCP-Name",
102
- ],
103
- exposedHeaders: ["WWW-Authenticate", "MCP-Protocol-Version"],
104
- }),
46
+ ActionHttp.layer(Http, greeting),
47
+ ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" }),
105
48
  );
106
49
  ```
107
50
 
108
- ### Subprocess
109
-
110
- ```ts example=mcp-stdio.ts
111
- import { NodeRuntime, NodeStdio } from "@effect/platform-node";
112
- import { Effect, Schema } from "effect";
113
- import * as Action from "@gjermundgaraba/effect-actions/Action";
114
- import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
115
- import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
51
+ See [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and
52
+ [quickstart/src/server.ts](../examples/quickstart/src/server.ts). Surfaces given the same
53
+ implementations in one set of routes run each builder once, so a note written over HTTP is read
54
+ as a tool: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). For two
55
+ servers, see [Action-in-process.md](Action-in-process.md#share-one-build-across-servers).
56
+
57
+ - The endpoint speaks MCP 2026-07-28, 2025-11-25 and 2025-06-18, as the client negotiates.
58
+ Claude Code 2.1 and Codex CLI 0.162 both connect, as tested in October 2026:
59
+ [notes-service](../examples/notes-service/README.md#run-it) has the commands,
60
+ [mcp-tools](../examples/mcp-tools/README.md#connect-a-client) those for stdio and Claude Desktop.
61
+ - **A client of a 2025 revision opens a session** with `initialize`; a 2026-07-28 request stands
62
+ alone. A session holds what its client sent there and no identity: each request authenticates,
63
+ as its own caller. It lasts until its client ends it with a `DELETE`, answered 204, or the process stops, as
64
+ Effect expires none, so limit request bodies, which bounds what one holds. Where an endpoint
65
+ serves a protected tool, only a signed-in caller opens one, and that client signs in for every
66
+ request, a public tool's call included. An endpoint of public tools alone lets anyone open them.
67
+ - An endpoint without `authentication` answers whoever reaches its port. Bind it to loopback or to
68
+ a network only trusted clients are on, and limit request bodies
69
+ ([ActionHttp.md](ActionHttp.md#serve-the-routes)):
70
+ [mcp-tools/src/server.ts](../examples/mcp-tools/src/server.ts).
71
+ - The endpoint takes no layer middleware. A limit that must hold for tools goes in the handlers:
72
+ [kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts).
73
+
74
+ ## Serve tools over stdio
75
+
76
+ Provide `Stdio` and the startup services, then run it:
116
77
 
117
- const Status = Action.make("status", {
118
- description: "Report whether the subprocess is ready.",
119
- success: { ready: Schema.Boolean },
120
- readOnly: true,
121
- caller: Action.Anyone,
122
- });
123
-
124
- const status = Action.implement(Status, () =>
125
- Effect.log("status called").pipe(Effect.as({ ready: true })),
126
- );
127
-
128
- // Serves until the host closes stdin, then exits 0. Protocol messages use stdout
129
- // exclusively: runStdio writes its program's Effect logs and `Console` output to stderr, and
130
- // `logToStderr`, applied last, does so for the layers provided around it, and reports a failure
131
- // there rather than as runMain would, on stdout.
132
- ActionMcp.runStdio(status, { name: "effect-actions-stdio", version: "0.1.0" }).pipe(
133
- Effect.provide(NodeStdio.layer),
78
+ ```ts
79
+ ActionMcp.runStdio([status, notes, identity], { name: "notes-service", version: "1.0.0" }).pipe(
80
+ // DEMO ONLY: a real host reads who started it, from its own login or configuration.
81
+ Effect.provideService(CurrentUser, users.ada),
82
+ Effect.provide([NodeStdio.layer, NoteStore.layerMemory]),
134
83
  ActionCli.logToStderr,
135
84
  NodeRuntime.runMain,
136
85
  );
137
86
  ```
138
87
 
139
- ### Native features
88
+ See [notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts), and
89
+ [mcp-tools/src/stdio.ts](../examples/mcp-tools/src/stdio.ts) for public tools.
140
90
 
141
- Effect's own resources, prompts and tools join an endpoint's tools as its `features`, one layer
142
- merging them:
91
+ To try a server by hand, send it one request, its 2026-07-28 metadata in `_meta` as
92
+ `Testing.mcpRequest` builds it. Over HTTP it also needs all five headers:
93
+
94
+ ```sh
95
+ BODY='{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"getStatus","arguments":{},"_meta":{"io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
96
+ echo "$BODY" | node --import tsx examples/notes-service/src/stdio.ts
97
+ curl http://127.0.0.1:3000/mcp -d "$BODY" -H 'Content-Type: application/json' \
98
+ -H 'Accept: application/json, text/event-stream' -H 'MCP-Protocol-Version: 2026-07-28' \
99
+ -H 'MCP-Method: tools/call' -H 'MCP-Name: getStatus'
100
+ ```
101
+
102
+ - **stdout carries the protocol alone.** `runStdio` sends its own program's Effect logs and
103
+ `Console` output to stderr. For the layers provided around it, apply `ActionCli.logToStderr`
104
+ last, before `runMain`: it moves their default logger and reports a startup failure there too.
105
+ Nothing moves the global `console.log`: keep it off stdout.
106
+ - **A subprocess has its own services.** It runs the handlers itself, so it shares a server's
107
+ data only through a store both reach, never one in memory. A host that should see what the
108
+ server holds connects to its HTTP endpoint.
109
+ - **No token arrives, so the host names the caller.** stdio takes no `authentication`: provide the
110
+ protected tools' identity around `runStdio`, whose one caller is whoever started it. Each
111
+ implementation's `authorize` still runs, so give that caller what it checks. Tools served
112
+ behind a token over HTTP keep their one protected contract here: never a public copy. An HTTP
113
+ server has many callers: never provide an identity at its startup, where it would stand for
114
+ every request.
115
+ - **Test the tools without a process**: the implementations in process over a fake upstream
116
+ ([mcp-tools/test/printers/printers.test.ts](../examples/mcp-tools/test/printers/printers.test.ts)),
117
+ an endpoint in memory
118
+ ([mcp-tools/test/routes.test.ts](../examples/mcp-tools/test/routes.test.ts)).
119
+
120
+ ## Describe tools to a model
121
+
122
+ A tool is its contract ([Action.md](Action.md#define-a-contract)): a model knows only the action's
123
+ name, its `description`, and its input schema with each field's `description`. `readOnly` is the
124
+ tool's read-only hint, and the contract's `mcp` option holds its `title`, its other hints and its
125
+ `_meta`. The server's `instructions` say what the tools are for together. See
126
+ [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts) and
127
+ [mcp-tools/src/instructions.ts](../examples/mcp-tools/src/instructions.ts).
128
+
129
+ - MCP takes a write as destructive unless it states `destructiveHint: false`.
130
+ - A tool's input is one object with keys. Omit `input` for a tool without arguments, and wrap a
131
+ union, an array or a scalar in a field.
132
+ - An argument the schema refuses, an undeclared one included, is answered by the native server
133
+ before the handler runs, as an `isError` result whose text is `Invalid parameters for tool
134
+ '<name>': …`, not `InvalidInput`. So constrain in the schema
135
+ what a model may send: an id written into an upstream URL is not made safe by
136
+ `encodeURIComponent`, which leaves `..` alone.
137
+ - A `Schema.makeFilter` check adds nothing to the tool's listed input schema, and
138
+ `Schema.isPattern` its `pattern` only for a RegExp with the `u` flag. Say such a rule in the
139
+ field's `description`, annotated before the check: after a custom one it is lost too.
140
+ - A declared error reaches the model as an `isError` result whose text is the error's JSON. Write
141
+ its `message` for the model, which acts on it:
142
+ [mcp-tools/src/printers/farm.ts](../examples/mcp-tools/src/printers/farm.ts). Every other case:
143
+ [guarantees.md](guarantees.md#wire-behavior).
144
+ - `actions` lists the tools, and only listed actions must suit MCP:
145
+ [kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts). Leave out an action a
146
+ model should not call, or one that takes a secret ([Action.md](Action.md#define-a-contract)).
147
+
148
+ ## Return images and other media
149
+
150
+ Give the success an `Action.Image` field, the one media kind, and return its bytes and MIME type
151
+ from the handler:
143
152
 
144
153
  ```ts
145
- ActionMcp.layerHttp(app, {
146
- name: "acme",
154
+ success: { printer: Printer, image: Action.Image },
155
+ ```
156
+
157
+ The tool sends the field as an image block, after the JSON of the rest. Every other surface sends
158
+ it as JSON, the bytes in base64. See
159
+ [mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts), and
160
+ [Testing.md](Testing.md#test-mcp-tools) for the client that decodes it back.
161
+
162
+ - An image is a top-level field of a struct success, bare, optional or a required array, or the
163
+ whole success or an array of it. Anywhere else in a served action, as in a `Schema.NullOr`, a
164
+ `Schema.Class` success or an input, `layerHttp` and `runStdio` throw. So an action taking an
165
+ image as input is no tool: leave it out of `actions`
166
+ ([kitchen-sink/src/attachments/Attachments.ts](../examples/kitchen-sink/src/attachments/Attachments.ts)).
167
+ - A media field's own `description` reaches no model. Describe the image in the action's
168
+ `description`.
169
+
170
+ ## Protect tools
171
+
172
+ Name the protected actions' authentication descriptor and provide its provider to the routes:
173
+
174
+ ```ts
175
+ const mcp = ActionMcp.layerHttp(implementations, {
176
+ name: "notes-service",
147
177
  version: "1.0.0",
148
- features: Layer.mergeAll(
149
- McpServer.resource({ uri: "docs://readme", name: "README", content: Effect.succeed("# Acme") }),
150
- McpServer.prompt({ name: "triage", content: () => Effect.succeed("Triage the issue.") }),
151
- ),
178
+ authentication: Login,
152
179
  });
180
+
181
+ export const routes = Layer.mergeAll(api, mcp, health, openApi).pipe(Layer.provide(authenticate));
153
182
  ```
154
183
 
155
- They are native: no implementation's authorization runs for them. On an endpoint
156
- naming a descriptor, every request a feature answers authenticates, its listings, completions
157
- and subscriptions included, so a public resource or prompt beside protected tools takes an
158
- endpoint of its own. The features register before the actions, and a native tool named as an
159
- action is refused when the endpoint builds: Effect's registry keeps one tool per name, and the
160
- endpoint's authentication decides by that name. A service they need, provided around the endpoint, is shared with the rest of the graph; one
161
- provided to the `features` layer itself is built once per endpoint.
162
-
163
- A feature runs with the services it was built with, never a request's: Effect's `resource` and
164
- `prompt` run their content in their build context alone, so a feature cannot read the caller.
165
- Content that depends on the caller is an action. Never provide an identity at startup to
166
- satisfy a feature.
167
-
168
- ## Rules
169
-
170
- - HTTP serves MCP 2026-07-28 and no other revision: it is stateless, every request standing alone. The stateful revisions keep a session per `initialize`, which Effect's HTTP runtime never expires and which no identity owns. Stdio serves 2026-07-28, 2025-11-25 and 2025-06-18, whichever the host negotiates. There is no `protocols` option; Effect owns version checks and negotiation.
171
- - `path` defaults to `/mcp`. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form.
172
- - Requests reaching the native MCP handler with an `Origin` header receive **403** unless that exact origin is listed in `allowedOrigins`. Requests without `Origin` pass this check. The endpoint's authentication runs first and may reject the request before native Origin validation; the allowlist does not protect authentication from untrusted-origin requests: a check that must run before it is global middleware ([Authentication.md](Authentication.md#rules)).
173
- - `allowedOrigins` is an Origin allowlist, not CORS configuration. Cross-origin browser clients also need outer CORS middleware or a proxy to handle preflight and add response headers. Without it, an allowed-origin `OPTIONS` request receives **405** and even a successful `POST` has no `Access-Control-Allow-Origin`. Keep preflight outside authentication and apply CORS headers to refusals too.
174
- - An endpoint serving a protected action names its descriptor, `authentication: Login`, and requires its provider, `Layer.provide(authenticate)`; the types refuse one without it, and `layerHttp` throws for plain JavaScript. One serving only public actions names none, and authenticates nothing.
175
- - An endpoint whose every tool is protected authenticates every request before reading its body, so a host signs in when it connects.
176
- - A mixed endpoint, serving public and protected tools on one URL, decides from the request's MCP routing headers before the native server reads its body, as the native server refuses a request whose headers disagree with its body. Discovery, `server/discover` and `tools/list`, `notifications/cancelled`, and a call of a public tool pass without a credential; a credential presented there is verified, and refused with 401 if it does not verify, while one its scheme decodes as empty passes as none. Every other request authenticates: a protected tool's call, before its arguments are decoded, and every native feature's request, `prompts/get`, `resources/read`, their listings, `completion/complete` and `subscriptions/listen` included, as does a method a later revision adds. The `Mcp-Name` header is compared as it is: a `tools/call` naming no public tool by it, a Base64-encoded name (`=?base64?...?=`) included, authenticates, and so does a request whose headers name no method: it fails closed.
177
- - So signed-out callers of a mixed endpoint list every tool, protected ones included, and call the public ones; a protected tool's call answers the 401 an MCP client signs in on, then retries. To keep protected tools unlisted to signed-out callers, serve the public tools on an endpoint of their own, with separate `layerHttp` calls on different paths.
178
- - A public tool never gets the identity, signed in or not; an action whose answer depends on the caller is protected. Middleware provided around `layerHttp` covers every request to the endpoint, before its authentication; an endpoint takes no layer middleware of its own, so middleware that reads the identity is not available over MCP ([ActionHttp.md](ActionHttp.md#rules)).
179
- - Every action of the implementations passed becomes a tool, named after the action, with the action's `mcp` hints, its `title` and `_meta` included, unless `actions` lists the tools: then only those, each behind its implementation's authorizer and from its builder's one run, and an implementation holding none of them is not built. To keep an action off MCP, leave it out of `actions`: `ActionMcp.layerHttp([users, pages], { name, version, actions: Tools })` ([Action.md](Action.md#contracts)). Only a listed action's input must suit MCP, and only listed actions' names must be unique. The types take only actions of the implementations, and owe only what the listed actions' handlers and authorizers, and the builders holding them, need. Whether the endpoint authenticates follows the listed actions alone.
180
- - Builders and request services follow the [dependency lifetimes](guarantees.md#dependency-lifetimes), and authorization the [authorization rules](guarantees.md#authorization). Only the tool registry is fresh per endpoint.
181
- - A handler may yield `McpServer.McpServer`, its endpoint's registry, to send notifications such as progress: the endpoint provides it, and no host owes it. Served on another surface too, where nothing provides it, read it with `Effect.serviceOption`.
182
- - Every served action's input must be one object with keys, as a tool's arguments are: fields, a struct or a class, identified, recursive or suspended, a record, or a declared type whose JSON Schema is an object. `layerHttp` and `runStdio` read the JSON Schema the native server reads, and throw for any other when they are called, naming the actions: a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Omit `input`, or give `{}`, for a tool with no arguments.
183
- - A tool's `inputSchema` is its input's JSON Schema, on every revision, closed with `additionalProperties: false` where the input declares its fields; a record's lists its value schema there instead. Undeclared arguments are refused on every revision, as invalid arguments.
184
- - A success is sent as it is. On 2026-07-28, over HTTP and stdio, it is `structuredContent: <encoded success>`, of any JSON type (`null` for an action that returns nothing), and one text block holding the same JSON, and the tool's `outputSchema` describes the encoded success. Stdio's earlier revisions, 2025-11-25 and 2025-06-18, structure less: they carry only an object as `structuredContent`, and list only an object-rooted `outputSchema`. A success they do not structure is text alone: its JSON, or a string success the string itself. A declared error is an `isError` result whose text content is the error's JSON encoding, the same bytes HTTP sends as the body, and no `structuredContent`.
185
- - On 2026-07-28, over HTTP and over stdio, the native server adds `_meta["io.modelcontextprotocol/serverInfo"]` and `resultType: "complete"` to every result, beside a tool result's own fields (`isError: false` on a success); the earlier revisions stdio speaks add neither. `serverInfo` is the options' `name`, `version`, `description`, `websiteUrl` and `icons`, as given, so a 2026-07-28 result's encoded size is the size of its own fields plus a fixed overhead per endpoint or subprocess.
186
- - Invalid arguments are answered by the native `McpServer`: from 2025-11-25 on, an `isError` result with a message for the model, such as `Invalid parameters for tool 'greet': Expected string\n at ["name"]`; on earlier stdio revisions, a JSON-RPC error. HTTP's `InvalidInput` does not apply. A protected tool's call from a caller without a credential that verifies gets the 401 first, before its arguments are decoded, malformed ones included, on a mixed endpoint as on one of protected tools alone.
187
- - Defects and encoding failures produce the generic `isError` text `Tool execution failed due to an internal server error.`; the cause is logged, not sent.
188
- - Every caller sees every tool of the endpoint ([guarantees.md](guarantees.md#scope)); authorization happens in each implementation's `authorize`.
189
- - Cancellation is Effect's native RPC interruption. Over HTTP there is no session, so `notifications/cancelled` interrupts nothing. A tool call ends with its HTTP request, whose lifetime (for example, whether a client disconnect interrupts it) is the host's.
190
- - Handlers may yield `McpSchema.McpRequestContext` for the client's declared information; the native server supplies it to every tool call, so it is never a router or host requirement.
191
- - `runStdio` is the subprocess's whole program: it serves until the host closes stdin, then succeeds, so the process exits 0. Closing stdin interrupts every call in flight; `runStdio` succeeds once they have stopped, after any uninterruptible region has completed. An interrupted call gets no result: no answer, or a JSON-RPC error. MCP hosts close stdin to shut a server down; a script piping requests keeps stdin open until it has read every answer. A signal interrupts it, as any program. Provide `Stdio` and its services to it and run it, `NodeRuntime.runMain`.
192
- - stdio is a local surface and takes no `authentication`: the host supplies `Stdio` (`NodeStdio.layer`) and any request-time services explicitly around `runStdio`, the identity of every protected tool included, `Effect.provideService(CurrentActor, actor)`. Each implementation's `authorize` runs for its protected tools. Tool arguments never establish identity.
193
- - `runStdio` gives its program a `Console` that writes every method through the `error` of the console it runs with, to stderr, since stdout carries the protocol: every console logger, the default one included, `Console.log`, and the counters, timers and group labels Node's console prints on stdout. A counter or a timer prints its label and its count or the milliseconds since it started, a group prints its label without indenting what follows, and `clear` does nothing. Layers provided around `runStdio` run outside its program: apply `ActionCli.logToStderr` last, before `runMain`, as the example does, which moves their default logger to stderr and reports there what `runMain` would report on stdout, a failure of such a layer or a defect, with the exit code `runMain` gives ([ActionCli.md](ActionCli.md#rules)). It does not move their `Console` output or a logger writing through `Console.log`, such as `Logger.consoleJson`; log JSON with `Logger.withConsoleError(Logger.formatJson)` instead. Keep the global `console.log` and other direct writes off stdout.
194
- - Each endpoint or subprocess owns a fresh native tool registry. That isolates tool names, not application context.
195
- - Every tool declares the built-in errors ([guarantees.md](guarantees.md#wire-behavior)). An authorizer's refusal is an `isError` result whose text is its JSON, `{"_tag":"Forbidden","message":"Not allowed."}`, exactly like an action's own error.
196
- - The authorizer runs after the native server decodes the tool's arguments.
197
- - On an endpoint naming a Bearer descriptor, a protected tool's step-up refusal is an HTTP 401 or 403 instead of a tool result ([guarantees.md](guarantees.md#authorization)); a public tool's is its `isError` result, signed in or not; without one, or under a descriptor of another scheme, a tool's refusal is an `isError` tool result, and only the authentication's own refusal is a 401 or 403. On a `Forbidden`'s `insufficient_scope` challenge, an MCP client re-authorizes with those scopes and retries.
198
- - That holds only while nothing of the response has been sent. Once a handler's notification, such as progress, has started a 200 event stream, a later refusal is the tool's `isError` result in it. An authorizer runs before its handler, so its refusal is always the status.
199
- - Authentication refuses before the MCP handler too: an HTTP 401 or 403. An MCP client reads the 401's `WWW-Authenticate` challenge and finds its authorization server through the discovery `Authentication.layer` publishes for a protected resource ([Authentication.md](Authentication.md)).
200
-
201
- ## Failure modes
202
-
203
- - `MCP tool input must be one object with keys, such as a struct: <name>, ...` thrown by `layerHttp` or `runStdio`: those actions' input is a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Wrap a union in a field, `input: { notification: Schema.Union([Email, Sms]) }`, omit `input` (or give `{}`) for no arguments, or leave the action out of `actions`.
204
- - `Type 'CurrentActor' is not assignable to type 'never'` where the server is launched, with `CurrentActor` among the endpoint's startup requirements rather than its `Request<"Requires", ...>`: a feature reads a request service, which it never receives ([native features](#native-features)). Serve that content as an action.
205
- - A native `McpServer.resource`, `McpServer.prompt` or `McpServer.toolkit` layer merged beside `layerHttp` builds without error and is never served: `resources/list` is empty, `prompts/list` is not found, and `tools/list` lists only the actions. Each endpoint's registry is its own: pass them as its `features`.
206
- - An MCP client gets an `isError` refusal instead of the 401 or 403 it re-authorizes on: the handler sent a notification before refusing, so the response had already started. Refuse in the implementation's `authorize`, before the handler runs.
207
- - `Duplicate MCP tool: <name>` thrown at the `layerHttp` or `runStdio` call: two implementations on one endpoint serve actions of the same name. Split the endpoint, or rename one action. `actions` tells apart only separate contracts sharing a name, listing one; two implementations of the same contract stay both listed: pass one implementation.
208
- - `Duplicate MCP tool: <name>, claimed by an action and a native feature`, a defect when the endpoint builds: a native tool among its `features` has an action's name. Rename the native tool, or leave the action out of `actions`.
209
- - `Listed in actions, but no implementation holds it: <names>` thrown at the `layerHttp` or `runStdio` call: those actions are not among the implementations' actions, 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.
210
- - `No overload matches this call` at `layerHttp`, ending `Type 'string' is not assignable to type 'never'` on its options: the endpoint serves a protected action and names no `authentication`, or one of another identity. Give its descriptor. `Protected action '<name>' requires its matching authentication descriptor` thrown by `layerHttp`: the same, from plain JavaScript.
211
- - `Provider<CurrentActor, "example.Login">` among the endpoint's requirements, or `Type 'Provider<...>' is not assignable to type 'never'` where the server is launched: the descriptor's provider is not provided. `Layer.provide(authenticate)` on that `layerHttp` ([Authentication.md](Authentication.md#failure-modes)).
212
- - `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in the endpoint's type: a handler or an authorizer yields a request service that no middleware around the endpoint provides. Provide it with router middleware on that `layerHttp`; never an identity at startup. `X` the identity, on an endpoint serving public tools: a public handler reads it, which no public tool gets. Make that action protected.
213
- - A public tool's call answers 401: it presented a credential that does not verify. Send a valid one, or none.
214
- - A protected tool's call answers 401 to a signed-in host after a while: its token expired; the host refreshes it, or signs in again, on the challenge's `invalid_token`.
215
- - A prompt or resource demands credentials: it is served on an endpoint naming a descriptor, where every feature request authenticates. Serve it on an endpoint without protected tools.
216
- - A stdio program owes `CurrentActor`, `Type 'CurrentActor' is not assignable to type 'never'` at `runMain`: it serves a protected tool, whose caller the host supplies. Provide it around `runStdio`; stdio takes no `authentication`.
217
- - Client reports a broken transport from a stdio subprocess: something wrote to stdout, such as `console.log`, or a layer provided around `runStdio` did, through the default logger without `Logger.LogToStderr` outermost, 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)`.
218
- - A stdio host asking for 2025-03-26 or 2024-11-05 is offered 2025-11-25 in the `initialize` result, as MCP negotiates, and disconnects if it cannot speak it. Update the host.
219
- - An MCP client cannot connect over HTTP: a request answers `400` with JSON-RPC error `-32020`. The client opens with `initialize`, as the 2025 revisions do, and HTTP serves only 2026-07-28. Pin the client to it (the official client: `versionNegotiation: { mode: { pin: "2026-07-28" } }`), or serve that host over stdio. Codex is such a client: it opens HTTP with `initialize` (measured on 0.159.0), so serve it over stdio.
220
- - An Origin-bearing request reaches the native handler and gets an empty 403: its `Origin` is not in `allowedOrigins`. Add the exact origin only if the deployment trusts it.
221
- - A disallowed Origin receives 401 instead: the endpoint's authentication rejected it before the native Origin check. A Host or Origin check that must run before authentication is global middleware ([Authentication.md](Authentication.md#rules)).
222
- - Browser calls fail despite an allowed Origin: configure CORS outside authentication and the MCP handler (see the browser example above). The native allowlist alone neither handles preflight nor adds CORS response headers.
223
- - `Object literal may only specify known properties, and 'protocols'`: the revisions are fixed. Delete the option.
184
+ See [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). Write the
185
+ [descriptor](Authentication.md#declare-how-callers-sign-in) and its
186
+ [provider](Authentication.md#verify-credentials) once for every surface, and
187
+ [publish discovery](Authentication.md#publish-oauth-discovery) so an MCP client finds where to
188
+ sign in. What a signed-in caller may do is each implementation's
189
+ [`authorize`](Action.md#authorize-calls), which runs on every call of a protected tool.
190
+
191
+ - **One endpoint or two.** An endpoint of protected tools alone authenticates every request,
192
+ the listing included. On a mixed endpoint, serving public and protected tools, a 2026-07-28
193
+ request lists every tool, protected ones included, and calls a public one signed out, and a
194
+ session's every request signs in ([Serve tools over HTTP](#serve-tools-over-http)). A
195
+ protected tool's call answers the 401 a client signs in on. To keep protected tools unlisted
196
+ to signed-out callers, or public ones open to a session's client, serve the public tools on an
197
+ endpoint of their own: a second `layerHttp` on another `path`.
198
+ - No listing is filtered by caller: `authorize` refuses the call, not the sight of the tool.
199
+ - On a Bearer endpoint, a protected tool's `Unauthenticated`, or `Forbidden` naming `scopes`,
200
+ answers the request with 401 or 403 and a challenge the client acts on, in place of a tool
201
+ result ([Authentication.md](Authentication.md#step-up-for-a-scope)). A `Forbidden` naming none
202
+ is an `isError` tool result, in a 200. Once a handler has sent a notification the response has
203
+ started, and its refusal is a tool result: refuse in `authorize`.
204
+ - Test as a signed-in caller ([Testing.md](Testing.md#test-as-a-signed-in-caller)):
205
+ [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts).
206
+ `Testing.mcpClient` decodes a refusal alike from a 401, a 403 or a tool result: to assert the
207
+ status, send `Testing.mcpRequest`.
208
+
209
+ ## Allow browser clients
210
+
211
+ A browser MCP client sends its page's `Origin`, and needs two things.
212
+
213
+ - **`allowedOrigins`.** A request carrying an `Origin` is refused with 403 unless that exact
214
+ origin is listed, so a page a user happens to visit cannot call the tools; one without, as
215
+ other clients send, passes. Read the list from configuration:
216
+ [mcp-tools/src/routes.ts](../examples/mcp-tools/src/routes.ts).
217
+ - **CORS.** `allowedOrigins` only refuses: it answers no preflight and adds no header. Add
218
+ `HttpRouter.cors`, global to the router, only where an origin is configured: given an empty
219
+ list, it answers `Access-Control-Allow-Origin: *`, and given one origin it names that origin
220
+ to every request, an unlisted page's included, which `allowedOrigins` still refuses. Allow the methods `POST` and
221
+ `DELETE`, the endpoint's only ones, and the headers `Content-Type`, `Authorization`,
222
+ `MCP-Protocol-Version`, `MCP-Method`, `MCP-Name` and `Mcp-Session-Id`; expose
223
+ `WWW-Authenticate`, `MCP-Protocol-Version` and `Mcp-Session-Id`: the same `routes.ts`, and
224
+ [kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts) with
225
+ authentication.
226
+
227
+ The endpoint's authentication runs before its Origin check, so `allowedOrigins` keeps no request
228
+ from the verifier. A `Host` allowlist, for a request that sends no `Origin`, or an Origin check
229
+ that must run first is a request policy:
230
+ [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
231
+
232
+ ## Add native prompts and resources
233
+
234
+ Merge Effect's own `McpServer.resource`, `McpServer.prompt` and `McpServer.toolkit` layers into
235
+ one and pass it as `features`:
236
+ [kitchen-sink/src/issues/guide.ts](../examples/kitchen-sink/src/issues/guide.ts), given in
237
+ [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and
238
+ [kitchen-sink/src/stdio.ts](../examples/kitchen-sink/src/stdio.ts). Provide a service they need
239
+ around the endpoint, never to the `features` layer itself: there it is built again for the
240
+ features alone, so a resource reads another store than the tools write.
241
+
242
+ - Merged beside the endpoint, not given as `features`, they build and are never served.
243
+ - No `authorize` runs for them, and they run with the services they were built with, never a
244
+ request's, so they cannot read the caller. Content that depends on the caller is an action.
245
+ - On an endpoint naming `authentication`, every prompt and resource request needs a credential,
246
+ listings included. To offer them to signed-out callers, serve them on an endpoint without
247
+ protected tools.
248
+
249
+ ## Send notifications from a handler
250
+
251
+ A handler yields `McpServer.McpServer` and sends through it:
252
+ `server.notifications["notifications/progress"]({ progressToken, progress })`.
253
+ `McpSchema.McpRequestContext` holds what the client sent: its `clientInfo`, and the request's
254
+ `_meta` as `requestMetadata`. The endpoint and `runStdio` provide both; an implementation also
255
+ served on another surface reads them with `Effect.serviceOption`.
256
+
257
+ - On 2026-07-28, a progress notification is sent only with the token its request carried,
258
+ `requestMetadata?.progressToken`. With another, or when the client sent none, it is dropped
259
+ silently.
@@ -0,0 +1,21 @@
1
+ # ActionRpc failure modes
2
+
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message.
4
+ The tasks are in [ActionRpc.md](ActionRpc.md).
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
+ - 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.
15
+ - `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
+ - `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.
21
+ - `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.