@gjermundgaraba/effect-actions 0.11.0 → 0.13.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 (215) hide show
  1. package/README.md +102 -41
  2. package/dist/Action.d.mts +31 -117
  3. package/dist/Action.mjs +14 -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 +23 -29
  9. package/dist/ActionMcp.mjs +56 -114
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +28 -9
  13. package/dist/ActionToolkit.mjs +31 -4
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +30 -23
  17. package/dist/Testing.mjs +27 -44
  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 +18 -0
  38. package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
  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 +211 -426
  46. package/docs/ActionCli-failures.md +104 -0
  47. package/docs/ActionCli.md +208 -293
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +205 -282
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +222 -271
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +179 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +103 -119
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +143 -392
  59. package/docs/CONTEXT.md +40 -24
  60. package/docs/README.md +75 -35
  61. package/docs/Testing-failures.md +21 -0
  62. package/docs/Testing.md +193 -151
  63. package/docs/guarantees.md +35 -96
  64. package/docs/setup-failures.md +37 -0
  65. package/docs/setup.md +90 -92
  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 +318 -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 +126 -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 +111 -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 +42 -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 +77 -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 +141 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +121 -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 +31 -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 +111 -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 +137 -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 +119 -0
  183. package/examples/notes-service/src/notes/handlers.ts +39 -0
  184. package/examples/notes-service/src/notes/store.ts +84 -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 +288 -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 +27 -9
  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 -14
  211. package/dist/internal/media.d.mts +0 -8
  212. package/dist/internal/media.mjs +0 -26
  213. package/dist/internal/refusal.mjs +0 -67
  214. package/dist/internal/schema-errors.mjs +0 -20
  215. package/dist/internal/tools.mjs +0 -44
@@ -1,14 +1,15 @@
1
- import { BuiltIn } from "./internal/errors.mjs";
2
- import { Call } from "./internal/call.mjs";
1
+ import { BuiltIn } from "./contract/errors.mjs";
2
+ import { Call } from "./contract/call.mjs";
3
3
  import { Action_d_exports, Any } from "./Action.mjs";
4
- import { Params } from "./internal/mcp.mjs";
5
- import { Served } from "./internal/memory.mjs";
6
- import { Effect, Layer, Schema } from "effect";
4
+ import { Params } from "./mcp/protocol.mjs";
5
+ import { Served } from "./testing/memory.mjs";
6
+ import { Cause, Effect, Layer, Schema } from "effect";
7
7
  import { Headers, HttpClient, HttpClientError, HttpClientRequest, HttpRouter } from "effect/http";
8
- //#region src/Testing.d.ts
8
+ //#region src/testing/Testing.d.ts
9
9
  /**
10
- * The native `HttpClient`, answered by `handler` instead of the network: a web handler the
11
- * test serves, which other tests may share, such as
10
+ * For a web handler the test already owns; most tests want `layer(routes)`, below, with
11
+ * `ActionHttp.client` and `mcpClient`. The native `HttpClient`, answered by `handler` instead of
12
+ * the network: a web handler the test serves, which other tests may share, such as
12
13
  * `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler`,
13
14
  * the routes' platform services provided, as `layer(routes)` provides them itself. It is the
14
15
  * client `layer(routes)` gives, without building anything: the test owns the handler, and
@@ -18,7 +19,10 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
18
19
  /**
19
20
  * The native `HttpClient`, answered in memory by `routes` instead of the network: provide
20
21
  * it to `ActionHttp.client` and to `mcpClient`. The routes are built with this layer and
21
- * released with its scope, without request logs. What they still require is this layer's, as
22
+ * released with its scope, without request logs: only the cause of a defect, of a result that
23
+ * does not encode or of a failure a route of your own leaves unhandled is logged, as an error,
24
+ * where a server's request logger would show it, on the process console even under `it.effect`
25
+ * and its `TestConsole`. What they still require is this layer's, as
22
26
  * under `HttpRouter.serve`: a builder's services, and a per-request service no middleware of
23
27
  * theirs provides, including one a global middleware reads. The routes keep their real
24
28
  * authentication: a client sends its caller's credential. Provided around it, the test
@@ -28,6 +32,8 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
28
32
  * the rest, whose `FileSystem` is a no-op. A relative URL resolves against `http://localhost`.
29
33
  * The client is the layer's own: the program's other HTTP clients get none of its requests,
30
34
  * and it none of theirs.
35
+ * Guide: `docs/Testing.md` in this package; in an application: `examples/notes-service/test/`.
36
+ * A type error at `it.effect`, such as one naming `Scope`: `docs/Testing-failures.md`.
31
37
  */
32
38
  export declare function layer<A, E, R>(routes: Layer.Layer<A, E, R>): Layer.Layer<HttpClient.HttpClient, E, Exclude<HttpRouter.Request.Without<R> | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>, Served>>;
33
39
  /** Where `mcpClient` sends, and through what client. */
@@ -37,7 +43,10 @@ export interface McpClientOptions {
37
43
  * the default `ActionMcp.layerHttp` path.
38
44
  */
39
45
  readonly url?: string;
40
- /** Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token, say. */
46
+ /**
47
+ * Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token is
48
+ * `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`.
49
+ */
41
50
  readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient;
42
51
  }
43
52
  /** Where an `mcpRequest` goes, and with what headers. */
@@ -52,19 +61,15 @@ export interface McpRequestOptions {
52
61
  }
53
62
  declare const McpCallError_base: Schema.Class<McpCallError, Schema.TaggedStruct<"McpCallError", {
54
63
  readonly message: Schema.String;
55
- }>, import("effect/Cause").YieldableError>;
64
+ }>, Cause.YieldableError>;
56
65
  /**
57
- * An answer a client method cannot decode as the action's success or a declared error,
58
- * such as the native server's message for invalid arguments or an unknown tool. Its message
59
- * holds the answer.
66
+ * An answer a client method cannot read as the action's success or a declared error, such as
67
+ * the native server's message for invalid arguments or an unknown tool, or a result without
68
+ * its structured content. Its message holds the answer. Structured content that does not
69
+ * decode as the success is a `SchemaError` instead, and a request that fails an
70
+ * `HttpClientError`.
60
71
  */
61
72
  export declare class McpCallError extends McpCallError_base {}
62
- /**
63
- * What one call of `A` fails with: a declared error value (the action's own or a built-in one
64
- * from the tool, or a refusal from the endpoint's authentication), a `SchemaError` when the
65
- * input does not encode or the success does not decode, an `HttpClientError` when the
66
- * endpoint could not be reached, or an `McpCallError` for any other answer.
67
- */
68
73
  type CallError<A extends Any> = A["error"][number]["Type"] | BuiltIn | Schema.SchemaError | HttpClientError.HttpClientError | McpCallError;
69
74
  /** Every action of `Actions` as `client.<action>(input)`, calling its tool. */
70
75
  export type McpClient<Actions extends ReadonlyArray<Any>> = { readonly [A in Actions[number] as A["name"]]: Call<A, Effect.Effect<A["success"]["Type"], CallError<A>>>; };
@@ -75,13 +80,15 @@ export type McpClient<Actions extends ReadonlyArray<Any>> = { readonly [A in Act
75
80
  * media field from the blocks whose `_meta` names it, and a success that is media, or whose
76
81
  * every field is, from its blocks alone. A declared error the tool returns, the action's own
77
82
  * or a refusal, is its decoded value, and so is a refusal the endpoint's authentication
78
- * answers with. The argument may be omitted when `{}` is a valid input. Requires the native
79
- * `HttpClient`, such as the one `layer` provides.
83
+ * answers with: a test telling a 401 or 403 from a tool result asserts the status of a request
84
+ * `mcpRequest` builds. The argument may be omitted when `{}` is a valid input. Requires the
85
+ * native `HttpClient`, such as the one `layer` provides.
80
86
  */
81
87
  export declare function mcpClient<const Actions extends ReadonlyArray<Any>>(actions: Actions, options?: McpClientOptions): Effect.Effect<McpClient<Actions>, never, HttpClient.HttpClient>;
82
88
  /**
83
89
  * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
84
- * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
90
+ * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
91
+ * `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
85
92
  * `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
86
93
  * `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
87
94
  * version is always the request's own. The test sends it: `HttpClient.execute` answers the
package/dist/Testing.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { Refusal } from "./internal/errors.mjs";
2
- import { assertOnce, projectedErrors } from "./internal/actions.mjs";
3
- import { inputOf } from "./internal/call.mjs";
4
- import { defaultPath, liftOf, statelessRequest } from "./internal/mcp.mjs";
5
- import { clientOf } from "./internal/memory.mjs";
6
- import { Context, Effect, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
7
- import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
1
+ import { Refusal } from "./contract/errors.mjs";
2
+ import { assertOnce, projectedErrors } from "./contract/rules.mjs";
3
+ import { checked } from "./contract/call.mjs";
4
+ import { defaultPath, liftOf, statelessRequest } from "./mcp/protocol.mjs";
5
+ import { clientOf } from "./testing/memory.mjs";
6
+ import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
8
7
  import { McpSchema } from "effect/ai";
8
+ import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
9
9
  import { Base64, Sse } from "effect/encoding";
10
- //#region src/Testing.ts
10
+ //#region src/testing/Testing.ts
11
11
  function layer(routes) {
12
12
  if (!Layer.isLayer(routes)) return clientOf(routes);
13
13
  return Layer.unwrap(Effect.gen(function* () {
@@ -15,27 +15,24 @@ function layer(routes) {
15
15
  const platform = yield* Layer.build(Layer.fresh(Layer.mergeAll(HttpPlatform.layer, Path.layer, Etag.layerWeak).pipe(Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, fileSystem)))));
16
16
  const context = Context.merge(platform, yield* Effect.context());
17
17
  const app = yield* HttpRouter.toHttpEffect(routes).pipe(Effect.provideContext(context));
18
- return clientOf(HttpEffect.toWebHandlerWith(context)(app));
18
+ return clientOf(HttpEffect.toWebHandlerWith(context)(Effect.tapCause(app, (cause) => isReported(cause) ? Effect.logError(cause).pipe(Effect.provideService(Console.Console, globalThis.console)) : Effect.void)));
19
19
  }));
20
20
  }
21
+ const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
21
22
  /**
22
- * An answer a client method cannot decode as the action's success or a declared error,
23
- * such as the native server's message for invalid arguments or an unknown tool. Its message
24
- * holds the answer.
23
+ * An answer a client method cannot read as the action's success or a declared error, such as
24
+ * the native server's message for invalid arguments or an unknown tool, or a result without
25
+ * its structured content. Its message holds the answer. Structured content that does not
26
+ * decode as the success is a `SchemaError` instead, and a request that fails an
27
+ * `HttpClientError`.
25
28
  */
26
29
  var McpCallError = class extends Schema.TaggedError()("McpCallError", { message: Schema.String }) {};
27
- /** The text of a block, none of a block of another kind. */
28
30
  const textOf = (block) => block?.type === "text" ? block.text : void 0;
29
- /** The JSON-RPC response to a `tools/call`: the native tool result, or a protocol error. */
30
- const ToolReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
31
+ const ToolCallReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
31
32
  code: Schema.Finite,
32
33
  message: Schema.String
33
34
  }) })]);
34
- const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolReply));
35
- /**
36
- * The reply in a response: its body when it is one JSON message, or the last reply among an
37
- * event stream's events, which notifications may precede.
38
- */
35
+ const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolCallReply));
39
36
  const replyOf = (response, text) => {
40
37
  if (!(response.headers["content-type"] ?? "").startsWith("text/event-stream")) return decodeReply(text);
41
38
  const data = [];
@@ -44,40 +41,25 @@ const replyOf = (response, text) => {
44
41
  }).feed(text);
45
42
  return Option.fromNullishOr(data.flatMap((message) => Option.toArray(decodeReply(message))).at(-1));
46
43
  };
47
- /**
48
- * The JSON of the media `field` a tool result's blocks hold: those of its kind naming it, or
49
- * none, for the whole success, in order; an array, or else the one, if any. Surplus blocks for
50
- * one value stay an array, which its decoding refuses.
51
- */
52
- const mediaOf = ({ name, kind, many }, result) => {
44
+ const mediaFromBlocks = ({ name, kind, many }, result) => {
53
45
  const values = result.content.flatMap((block) => block.type === kind && block._meta?.["effect-actions/field"] === name ? [{
54
46
  data: Base64.encode(block.data),
55
47
  mimeType: block.mimeType
56
48
  }] : []);
57
49
  return many || values.length > 1 ? values : values[0];
58
50
  };
59
- /**
60
- * The encoded success of a tool result lifting the media `lift` finds: the whole success's
61
- * blocks, or its structured content with each media field taken from its blocks alone, an
62
- * empty one when nothing else was left.
63
- */
64
- const reassembled = (lift, result) => {
51
+ const encodedSuccessOf = (lift, result) => {
65
52
  const [whole] = lift.fields;
66
- if (whole !== void 0 && whole.name === void 0) return mediaOf(whole, result);
53
+ if (whole !== void 0 && whole.name === void 0) return mediaFromBlocks(whole, result);
67
54
  const { structuredContent = {} } = result;
68
55
  if (!Predicate.isObject(structuredContent)) return structuredContent;
69
56
  const lifted = new Set(lift.fields.map(({ name }) => name));
70
57
  return Object.fromEntries([...Object.entries(structuredContent).filter(([key]) => !lifted.has(key)), ...lift.fields.flatMap((field) => {
71
- const value = mediaOf(field, result);
58
+ const value = mediaFromBlocks(field, result);
72
59
  return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
73
60
  })]);
74
61
  };
75
- /** Fail with the value of one of `errors` that `text` holds, or else with `otherwise`. */
76
62
  const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
77
- /**
78
- * The tool call of `action`, whose success its tool lifts as `lift` says, with `input` on
79
- * `client`, sending to `url`.
80
- */
81
63
  const callTool = (client, url, action, lift, input) => {
82
64
  const { name } = action;
83
65
  const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
@@ -101,7 +83,7 @@ const callTool = (client, url, action, lift, input) => {
101
83
  return yield* failWith(projectedErrors(action), error, other(`returned an error: ${error}`));
102
84
  }
103
85
  if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
104
- return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(reassembled(lift, result));
86
+ return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(encodedSuccessOf(lift, result));
105
87
  });
106
88
  };
107
89
  function mcpClient(actions, { url = defaultPath, transformClient = identity } = {}) {
@@ -110,13 +92,14 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
110
92
  const client = transformClient(native);
111
93
  return Object.fromEntries(actions.map((action) => {
112
94
  const lift = liftOf(action.success);
113
- return [action.name, (...args) => Effect.flatMap(inputOf(action, args), (input) => callTool(client, url, action, lift, input))];
95
+ return [action.name, checked(action, (input) => callTool(client, url, action, lift, input))];
114
96
  }));
115
97
  });
116
98
  }
117
99
  /**
118
100
  * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
119
- * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
101
+ * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
102
+ * `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
120
103
  * `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
121
104
  * `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
122
105
  * version is always the request's own. The test sends it: `HttpClient.execute` answers the
@@ -126,8 +109,8 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
126
109
  * a call its types would not send.
127
110
  */
128
111
  const mcpRequest = (method, params = {}, { headers = {}, url = defaultPath } = {}) => {
129
- const { headers: routing, body } = statelessRequest(method, params);
130
- return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routing), HttpClientRequest.bodyJsonUnsafe(body));
112
+ const { headers: routingHeaders, body } = statelessRequest(method, params);
113
+ return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routingHeaders), HttpClientRequest.bodyJsonUnsafe(body));
131
114
  };
132
115
  //#endregion
133
116
  export { McpCallError, layer, mcpClient, mcpRequest };
@@ -1,11 +1,11 @@
1
- import { Anyone } from "./actions.mjs";
2
- import { Refusal } from "./errors.mjs";
3
- import { Protected, ServedRequest, Serving } from "./implementation.mjs";
1
+ import { Anyone } from "../contract/rules.mjs";
2
+ import { Refusal } from "../contract/errors.mjs";
3
+ import { BuildServices, Protected, ServedRequest, Serving } from "../contract/implementation.mjs";
4
4
  import { Action_d_exports, Any as Any$1, Errors } from "../Action.mjs";
5
5
  import { Context, Effect } from "effect";
6
6
  import { HttpRouter, HttpServerResponse } from "effect/http";
7
7
  import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
- //#region src/internal/authentication.d.ts
8
+ //#region src/authentication/provider.d.ts
9
9
  /** One native security scheme: Bearer unless a descriptor names another. */
10
10
  export type Security = HttpApiSecurity.HttpApiSecurity;
11
11
  /** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
@@ -34,6 +34,12 @@ type Response = Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, un
34
34
  interface Runtime$1 {
35
35
  /** The descriptor the provider was built from, the one a binding naming it must hold. */
36
36
  readonly descriptor: unknown;
37
+ /**
38
+ * The verifier itself, for a surface answering refusals its own way, such as RPC: the
39
+ * credential to the identity, or a refusal, or an error the descriptor declares. An empty
40
+ * credential, as the native decoder gives an absent one, is refused without reaching it.
41
+ */
42
+ readonly authenticate: (credential: Credential) => Effect.Effect<unknown, unknown, unknown>;
37
43
  /** The native security middleware's implementation, keyed by its scheme. */
38
44
  readonly middleware: Readonly<Record<string, (route: Response, options: {
39
45
  readonly credential: Credential;
@@ -43,6 +49,11 @@ interface Runtime$1 {
43
49
  * answered with its challenge under Bearer; as it is under another scheme.
44
50
  */
45
51
  readonly stepUp: HttpApiMiddleware.HttpApiMiddleware<never, never, never>;
52
+ /**
53
+ * An MCP endpoint's request: decoded here, and a protected tool's call promotes what it
54
+ * verified. Where `optional`, a request presenting no credential, which the scheme decodes as
55
+ * empty, passes signed out.
56
+ */
46
57
  readonly http: <E, R>(route: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>, optional: boolean) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R | HttpRouter.Provided>;
47
58
  /**
48
59
  * A route of the host's own: the request is decoded here, the route gets the identity
@@ -61,7 +72,7 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
61
72
  readonly security: S;
62
73
  /**
63
74
  * What its verifier may fail with besides a refusal, as a list: every protected endpoint
64
- * declares it, and every remote surface sends it with its status.
75
+ * declares it, and every remote surface sends it, over HTTP with its status.
65
76
  */
66
77
  readonly error: E;
67
78
  readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
@@ -69,12 +80,12 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
69
80
  /** An MCP request's verified identity, which a protected tool's call promotes. */
70
81
  readonly "~verified": Context.Key<never, unknown>;
71
82
  }
72
- export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
73
- export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
74
83
  /**
75
- * The identity a descriptor for `A` must authenticate: `unknown` for an erased action, whose
76
- * key is known only at run time, where `assertAuthentication` checks it.
84
+ * Any descriptor `make` returns, named so a package emitting declarations can export one, and a
85
+ * binding name one.
77
86
  */
87
+ export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
88
+ export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
78
89
  type Covered<A extends Any$1> = Exclude<A["caller"], typeof Anyone> extends Context.Key<infer I, unknown> ? I : never;
79
90
  export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
80
91
  readonly authentication?: Any;
@@ -82,6 +93,16 @@ export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
82
93
  readonly authentication: Descriptor<Covered<Protected<A>>, unknown, Security, string, Errors>;
83
94
  };
84
95
  export type ProviderOf<D> = D extends Descriptor<infer I, unknown, Security, infer Name, Errors> ? Provider<I, Name> : never;
96
+ /** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
97
+ export type PublicOnly<Actions extends ReadonlyArray<Any$1>> = [Exclude<Actions[number], {
98
+ readonly caller: typeof Anyone;
99
+ }>] extends [never] ? unknown : {
100
+ readonly "Protected actions take options naming their authentication": never;
101
+ };
102
+ /** The descriptor options `O` name, where they always name one. */
103
+ export type DescriptorOf<O> = [O] extends [{
104
+ readonly authentication: infer D extends Any;
105
+ }] ? D : undefined;
85
106
  /** All protected actions on a surface use the descriptor's single identity key. */
86
107
  export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>> ? unknown : [Exclude<Covered<Protected<A>>, D extends Descriptor<infer I, unknown, Security, string, Errors> ? I : never>] extends [never] ? unknown : {
87
108
  readonly "Authentication descriptor does not cover every protected action": never;
@@ -90,6 +111,17 @@ export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>>
90
111
  export type RemoteRequest<App, A extends Any$1> = ServedRequest<App, Extract<A, {
91
112
  readonly caller: typeof Anyone;
92
113
  }>> | Exclude<ServedRequest<App, Protected<A>>, Identity<Protected<A>>>;
114
+ /**
115
+ * What a server of the actions `A` of the implementations `App` reads at startup, as
116
+ * `BuildServices` states it, with the caller a builder yields named as the mistake it is: a server
117
+ * has its caller per request.
118
+ */
119
+ export type ServerBuildServices<App, A extends Any$1> = App extends unknown ? BuildServices<App, A> extends (infer R) ? R extends Identity<Protected<A>> ? {
120
+ readonly "A builder yields the caller, which a server has per request: yield it in a handler or an authorizer": {
121
+ readonly caller: R;
122
+ readonly builderOf: Serving<App, A>["name"];
123
+ };
124
+ } : R : never : never;
93
125
  /**
94
126
  * What a call of `A` under the descriptor `D` may fail with from its verifier: the errors `D`
95
127
  * declares, for a protected action; none for a public one, which nothing verifies.
@@ -0,0 +1,12 @@
1
+ import { Unauthenticated } from "../contract/errors.mjs";
2
+ import { Anyone } from "../contract/rules.mjs";
3
+ import { Effect, Option } from "effect";
4
+ //#region src/authentication/provider.ts
5
+ const withVerifierErrors = (action, errors, verifierErrors) => action.caller === Anyone || verifierErrors === void 0 ? errors : [...errors, ...verifierErrors];
6
+ const assertAuthentication = (actions, auth) => {
7
+ for (const action of actions) if (action.caller !== Anyone && (auth === void 0 || action.caller.key !== auth.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
8
+ };
9
+ const providerOf = (auth) => Effect.flatMap(Effect.service(auth["~provider"]), (provider) => provider.descriptor === auth ? Effect.succeed(provider) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the binding's descriptor is not its provider's; build both from one descriptor`)));
10
+ const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
11
+ //#endregion
12
+ export { assertAuthentication, promote, providerOf, withVerifierErrors };
@@ -0,0 +1,36 @@
1
+ import { Refusal, statuses } from "../contract/errors.mjs";
2
+ import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+ //#region src/authentication/refusal.ts
5
+ const rfc6750ErrorDescriptionCharacters = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
6
+ const challenge = (scheme, parameters) => {
7
+ const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
8
+ return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
9
+ };
10
+ const bearer = (parameters) => challenge("Bearer", parameters);
11
+ const encode = Schema.encodeSync(Refusal);
12
+ const isRefusal = Schema.is(Refusal);
13
+ const insufficientScopeChallenge = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
14
+ ["error", "insufficient_scope"],
15
+ ["scope", error.scopes.join(" ")],
16
+ ["resource_metadata", metadataUrl],
17
+ ["error_description", rfc6750ErrorDescriptionCharacters.test(error.message) ? error.message : void 0]
18
+ ]) : void 0;
19
+ const plain = (error) => HttpServerResponse.jsonUnsafe(encode(error), { status: statuses[error._tag] });
20
+ const answer = (error, metadataUrl) => {
21
+ const response = plain(error);
22
+ const scoped = insufficientScopeChallenge(error, metadataUrl);
23
+ return scoped === void 0 ? response : HttpServerResponse.setHeader(response, "www-authenticate", scoped);
24
+ };
25
+ var SteppedUpRefusal = class extends Context.Service()("effect-actions/SteppedUp") {};
26
+ const isStepUp = (error) => isRefusal(error) && (!Predicate.isTagged(error, "Forbidden") || error.scopes !== void 0);
27
+ const recordStepUp = (call) => Effect.tapError(call, (error) => isStepUp(error) ? Effect.flatMap(Effect.serviceOption(SteppedUpRefusal), (slot) => Option.isSome(slot) ? Ref.set(slot.value, Option.some(error)) : Effect.void) : Effect.void);
28
+ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
29
+ const slot = yield* Ref.make(Option.none());
30
+ const exit = yield* Effect.exit(Effect.provideService(route, SteppedUpRefusal, slot));
31
+ const refused = yield* Ref.get(slot);
32
+ const diedOrInterruptedAfterRefusal = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
33
+ return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
34
+ });
35
+ //#endregion
36
+ export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
@@ -1,14 +1,11 @@
1
- import "./errors.mjs";
1
+ import "../contract/errors.mjs";
2
2
  import { Action_d_exports, Any } from "../Action.mjs";
3
3
  import { Effect, Runtime, Types } from "effect";
4
4
  import { CliError, Command } from "effect/cli";
5
- //#region src/internal/cli.d.ts
6
- /**
7
- * The fields of `A`'s input that may be positional: the named top-level fields of a struct
8
- * or class input, as encoded. None for any other input, a record or a union included.
9
- */
10
- export type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
11
- /** How one command is named, takes its input and prints its result. */
5
+ //#region src/cli/commands.d.ts
6
+ type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
7
+ type StdinField<A extends Any> = A["input"]["Encoded"] extends (infer E) ? Exclude<Field<A>, { [K in keyof E]-?: {} extends Pick<E, K> ? K : never; }[keyof E]> : never;
8
+ /** How one command of `A` is named, takes its input and prints its result. */
12
9
  export interface Options<A extends Any> {
13
10
  /** Override the command name. The action name, in kebab case, is used by default. */
14
11
  readonly name?: string;
@@ -24,12 +21,19 @@ export interface Options<A extends Any> {
24
21
  * `Param.withAlias` takes it.
25
22
  */
26
23
  readonly aliases?: { readonly [K in Field<A>]?: string; };
24
+ /**
25
+ * A required field read from stdin instead of a flag, such as a secret, which a flag would
26
+ * leave in the process list and the shell's history.
27
+ */
28
+ readonly stdin?: StdinField<A>;
27
29
  }
28
30
  /**
29
- * What a command fails with when its action fails: Effect CLI's own error for a handler's
30
- * failure, which `Command.run` prints on stderr and marks reported, so `runMain` does not
31
- * print it again. Its `cause` is the action's failure. The process exits with the cause's
32
- * `Runtime.errorExitCode`, 1 by default.
31
+ * What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
32
+ * action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
33
+ * prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
34
+ * the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
35
+ * reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
36
+ * fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
33
37
  */
34
38
  export declare class UserError<out E = unknown> extends CliError.UserError {
35
39
  readonly cause: E;