@gjermundgaraba/effect-actions 0.6.0 → 0.8.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.
- package/README.md +3 -3
- package/dist/Action.d.mts +29 -8
- package/dist/Action.mjs +2 -2
- package/dist/ActionCatalog.d.mts +2 -3
- package/dist/ActionCatalog.mjs +8 -6
- package/dist/ActionCli.d.mts +1 -1
- package/dist/ActionCli.mjs +1 -1
- package/dist/ActionCliClient.d.mts +11 -12
- package/dist/ActionCliClient.mjs +7 -14
- package/dist/ActionGroup.d.mts +1 -1
- package/dist/ActionGroup.mjs +3 -9
- package/dist/ActionHttp.d.mts +5 -7
- package/dist/ActionHttp.mjs +8 -6
- package/dist/ActionHttpClient.d.mts +8 -20
- package/dist/ActionHttpClient.mjs +8 -16
- package/dist/ActionMcp.d.mts +8 -7
- package/dist/ActionMcp.mjs +73 -4
- package/dist/ActionToolkit.d.mts +11 -12
- package/dist/ActionToolkit.mjs +1 -1
- package/dist/Authentication.d.mts +1 -1
- package/dist/Authentication.mjs +1 -1
- package/dist/Testing.d.mts +11 -9
- package/dist/Testing.mjs +10 -9
- package/dist/internal/actions.d.mts +1 -1
- package/dist/internal/cli.d.mts +1 -1
- package/dist/internal/cli.mjs +1 -1
- package/dist/internal/fetchClient.d.mts +6 -0
- package/dist/internal/fetchClient.mjs +8 -0
- package/dist/internal/implementation.d.mts +14 -15
- package/dist/internal/implementation.mjs +3 -3
- package/dist/internal/tools.d.mts +2 -2
- package/dist/internal/tools.mjs +16 -14
- package/docs/Action.md +27 -14
- package/docs/ActionCatalog.md +10 -10
- package/docs/ActionCli.md +8 -8
- package/docs/ActionCliClient.md +9 -9
- package/docs/ActionGroup.md +10 -8
- package/docs/ActionHttp.md +13 -13
- package/docs/ActionHttpClient.md +26 -19
- package/docs/ActionMcp.md +9 -6
- package/docs/ActionToolkit.md +5 -5
- package/docs/Authentication.md +2 -3
- package/docs/CONTEXT.md +6 -6
- package/docs/README.md +4 -4
- package/docs/Testing.md +8 -5
- package/docs/guarantees.md +5 -4
- package/docs/setup.md +6 -5
- package/package.json +11 -6
package/docs/ActionCli.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# ActionCli
|
|
2
2
|
|
|
3
3
|
Native Effect CLI commands that run handlers in-process. The host provides every service;
|
|
4
|
-
nothing is fetched over HTTP.
|
|
4
|
+
nothing is fetched over HTTP. Any action runs locally, whether or not HTTP or MCP serves it.
|
|
5
5
|
|
|
6
6
|
## API
|
|
7
7
|
|
|
8
8
|
Import `@gjermundgaraba/effect-actions/ActionCli`.
|
|
9
9
|
|
|
10
|
-
| API | Purpose
|
|
11
|
-
| ------------------------------ |
|
|
12
|
-
| `command(app, name, options?)` | One selected action as a native Effect `Command`.
|
|
13
|
-
| `group(app, options?)` | Every action under a group command
|
|
14
|
-
| `Options`, `GroupOptions` | Configuration for single and aggregate commands.
|
|
10
|
+
| API | Purpose |
|
|
11
|
+
| ------------------------------ | ------------------------------------------------- |
|
|
12
|
+
| `command(app, name, options?)` | One selected action as a native Effect `Command`. |
|
|
13
|
+
| `group(app, options?)` | Every action under a group command. |
|
|
14
|
+
| `Options`, `GroupOptions` | Configuration for single and aggregate commands. |
|
|
15
15
|
|
|
16
16
|
| Option | Meaning |
|
|
17
17
|
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
@@ -28,7 +28,7 @@ scope. Success output is encoded; failures remain failures of the command Effect
|
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
30
|
import { Console, Effect, Logger } from "effect";
|
|
31
|
-
import { Command, Flag } from "effect/
|
|
31
|
+
import { Command, Flag } from "effect/cli";
|
|
32
32
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
33
33
|
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
34
34
|
import { actors, authorize, CurrentActor } from "./auth.js";
|
|
@@ -56,7 +56,7 @@ Command.runWith(command, { version: "0.1.0" })(process.argv.slice(2)).pipe(
|
|
|
56
56
|
|
|
57
57
|
## Rules
|
|
58
58
|
|
|
59
|
-
- `command(app, name)` selects one action by name from the bound group; the name is checked at the type level. `group(app)` puts every action under the group command, including actions with `
|
|
59
|
+
- `command(app, name)` selects one action by name from the bound group; the name is checked at the type level. `group(app)` puts every action under the group command, including actions with `mcp: false` and groups no HTTP binding serves.
|
|
60
60
|
- Default syntax is `--input '<json>'` or `--input-file <path>` carrying the entire encoded input: nested objects, arrays, scalars. The file is read and JSON-parsed by the native `Flag.FileSchema`. Both are decoded by the native parser; if both are given, the file is used. Omitting both decodes `{}` separately on every invocation; result identity follows the codec's behavior. An action that requires input then fails with a `SchemaError`, exactly as a `parameters` command's mapped input does.
|
|
61
61
|
- `parameters` and `input` are supplied together or not at all. With them, the command has explicit native flags and arguments and neither `--input` nor `--input-file`, so its syntax does not change when the schema changes.
|
|
62
62
|
- `input(parsed)` returns encoded JSON. The action schema decodes it before dispatch; a mismatch is a `SchemaError` at runtime. Action fields are never turned into flags automatically.
|
package/docs/ActionCliClient.md
CHANGED
|
@@ -9,8 +9,8 @@ Import `@gjermundgaraba/effect-actions/ActionCliClient`.
|
|
|
9
9
|
|
|
10
10
|
| API | Purpose |
|
|
11
11
|
| ------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
12
|
-
| `command(http, groupName, actionName, options?)` | Call one
|
|
13
|
-
| `group(http, groupName, options?)` | Every
|
|
12
|
+
| `command(http, groupName, actionName, options?)` | Call one action of the binding through a native Effect `Command`. |
|
|
13
|
+
| `group(http, groupName, options?)` | Every action of a bound group under one command. |
|
|
14
14
|
| `Options`, `GroupOptions`, `Connection` | Single/aggregate command configuration and native HTTP connection options. |
|
|
15
15
|
|
|
16
16
|
Single-command options: `name`, `render`, paired `parameters`/`input`, and `connection`.
|
|
@@ -25,7 +25,7 @@ retain the selected endpoint's client failures and middleware requirements, and
|
|
|
25
25
|
## Canonical
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
import { Command } from "effect/
|
|
28
|
+
import { Command } from "effect/cli";
|
|
29
29
|
import { Console, Effect, Logger } from "effect";
|
|
30
30
|
import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
31
31
|
import * as ActionCliClient from "@gjermundgaraba/effect-actions/ActionCliClient";
|
|
@@ -49,15 +49,15 @@ Authentication: `connection: { transformClient: HttpClient.mapRequest(HttpClient
|
|
|
49
49
|
|
|
50
50
|
## Rules
|
|
51
51
|
|
|
52
|
-
- Selectors resolve from `http.groups` only. `groupName` must be a bound group; `actionName` must be an action of that group
|
|
52
|
+
- Selectors resolve from `http.groups` only. `groupName` must be a bound group; `actionName` must be an action of that group. Runtime guards reject dynamically supplied strings that the types would not accept.
|
|
53
53
|
- The host provides `HttpClient` and its configuration. Endpoint selection, credentials, and storage are the host's; nothing is inferred from action arguments.
|
|
54
|
-
- Input is
|
|
54
|
+
- Input (`--input`, `--input-file`, or `input(parsed)`) is encoded JSON. The command decodes it with the action schema, and the native client re-encodes it with `Http.api`'s closed payload codec.
|
|
55
55
|
- Input syntax (`--input`, `--input-file`, or `parameters`) and output (`render` and its `--json` flag) are exactly as in `ActionCli`.
|
|
56
|
-
- Errors are the client's: declared errors, policy errors, `SchemaError`, and `HttpClientError`.
|
|
56
|
+
- Errors are the client's: declared errors (the binding's `errors` included), policy errors, `SchemaError`, and `HttpClientError`.
|
|
57
57
|
|
|
58
58
|
## Failure modes
|
|
59
59
|
|
|
60
|
-
- Type error on `actionName`: the action is
|
|
61
|
-
- `HttpClient`
|
|
62
|
-
-
|
|
60
|
+
- Type error on `actionName`: the action is not in that group, or the group is not bound in this `Http`.
|
|
61
|
+
- `HttpClient` unsatisfied (a type error at `runMain`): provide `NodeHttpClient.layerUndici` (or `FetchHttpClient.layer`) to the runtime.
|
|
62
|
+
- `HttpClientError` with `reason._tag` `InvalidUrlError`: `connection.baseUrl` is absent; it has no default. Connection refused: `baseUrl` points at the wrong host or port.
|
|
63
63
|
- 401 from the server: add `transformClient` to `connection`. The command adds no headers on its own.
|
package/docs/ActionGroup.md
CHANGED
|
@@ -31,7 +31,7 @@ builder's requirements.
|
|
|
31
31
|
import { Effect, Schema } from "effect";
|
|
32
32
|
import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
|
|
33
33
|
import { CurrentActor } from "./auth.js";
|
|
34
|
-
import { GetUser, RenameUser, WhoAmI } from "./contracts.js";
|
|
34
|
+
import { Double, GetUser, RenameUser, WhoAmI } from "./contracts.js";
|
|
35
35
|
import { Users } from "./users.js";
|
|
36
36
|
|
|
37
37
|
class InvalidRequest extends Schema.TaggedError<InvalidRequest>()(
|
|
@@ -58,6 +58,8 @@ const schemaError = {
|
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
export const MathActions = ActionGroup.make({ name: "math" }, Double);
|
|
62
|
+
|
|
61
63
|
export const UserActions = ActionGroup.make(
|
|
62
64
|
{ name: "users", schemaError },
|
|
63
65
|
GetUser,
|
|
@@ -91,10 +93,10 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
91
93
|
|
|
92
94
|
## Rules
|
|
93
95
|
|
|
94
|
-
- `name` matches `[A-Za-z0-9_-]
|
|
96
|
+
- `name` matches `[A-Za-z0-9_-]+` and cannot be `then`. Action names are unique within the group. Group and MCP tool names are checked where they are served; see [guarantees.md](guarantees.md#namespaces).
|
|
95
97
|
- Group `errors` are appended to each action's own `errors`, so every handler of the group may fail with them. A failure that only the surface produces belongs on the adapter instead (`ActionHttp.make`'s `errors`, `ActionMcp`'s and `ActionToolkit`'s `errors`), not here.
|
|
96
|
-
- `implement` takes a complete record or an Effect producing one. Every action must have a handler; missing keys are compile errors. A record that still lacks one at runtime (plain JavaScript, a cast) throws at `implement`; a builder's record fails the adapter layer's build. Nothing is served with a handler missing. Additional keys are not dispatched. It takes nothing else.
|
|
97
|
-
- The builder Effect runs once per adapter layer that serves the implementation, in that layer's scope
|
|
98
|
+
- `implement` takes a complete plain record or an Effect producing one. Every action must have a handler; missing keys are compile errors. A handler is an own-property function of the record: an inherited one, such as a class instance's method, does not count. A record that still lacks one at runtime (plain JavaScript, a cast: a missing key, `undefined`, any other non-function, an inherited method) throws at `implement`; a builder's record fails the adapter layer's build. Nothing is served with a handler missing. Handlers are called without a receiver. Additional keys are not dispatched. It takes nothing else.
|
|
99
|
+
- The builder Effect runs once per adapter layer that serves the implementation, in that layer's scope, and once per invocation under `ActionCli`. An implementation served by HTTP and MCP is built twice. Acquire shared state in a Layer you provide to the adapters, not in the builder.
|
|
98
100
|
- Services yielded in the builder are build-time requirements (`RX`). Services yielded in a handler are request-time requirements (`R` of the handler). Adapters keep these separate in their types. Use distinct tags for each kind; never provide a request-identity tag at startup (see [guarantees.md](guarantees.md)).
|
|
99
101
|
- `Implementation` is nominal. Spreading its properties does not produce an implementation. Each `implement` call is a separate binding.
|
|
100
102
|
- `build` exists for direct handler tests under `Effect.scoped`. It bypasses transport validation and the hook, and is not a substitute for adapter tests. There is no public handler service or Layer.
|
|
@@ -109,18 +111,18 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
109
111
|
- MCP is unaffected. The native `McpServer` answers invalid arguments and unencodable results itself.
|
|
110
112
|
- Policy errors extend the transport contract, not the handler contract. A handler cannot return them.
|
|
111
113
|
- The policy runs only on the server. Client-side codec failures stay `SchemaError`.
|
|
112
|
-
- HTTP decodes with `errors: "all"`, so `cause` carries every issue. Issues never retain the rejected values.
|
|
114
|
+
- HTTP decodes input with `errors: "all"`, so an `invalid` `cause` carries every issue. Issues never retain the rejected values.
|
|
113
115
|
- Groups served by one adapter may have different policies; each action answers with its own group's.
|
|
114
116
|
- Input schema-error policies run before the `before` hook. Invalid input is answered by the policy without invoking the hook or handler.
|
|
115
117
|
- Domain errors, defects, interruptions, and protocol errors are not remapped. An unencodable declared error is a defect. A broken policy error is not recursively remapped.
|
|
116
118
|
|
|
117
119
|
## Failure modes
|
|
118
120
|
|
|
119
|
-
- Throws at `make`:
|
|
121
|
+
- Throws at `make`: `Duplicate action: <name>` or `Invalid action group name: <name>`.
|
|
120
122
|
- `Property 'x' is missing in type` at `implement`: the record lacks a handler for action `x`.
|
|
121
123
|
- Handler compiles but returns an error not in `errors`: type error on the handler's error channel. Declare it on the action or the group.
|
|
122
124
|
- `Expected 1 arguments, but got 2` at `implement`: the second options object is gone. Move `before` to the adapter that serves the group.
|
|
123
|
-
- `Parameter 'failure' implicitly has an 'any' type` in a standalone policy constant: a `make` that reads the failure is typed only inline. Annotate the parameter as `HttpApiError.HttpApiSchemaError` (type import from `effect/
|
|
124
|
-
- `Missing handlers for group "<group>": <actions>` thrown at `implement`, or failing the layer build: the handler record
|
|
125
|
+
- `Parameter 'failure' implicitly has an 'any' type` in a standalone policy constant: a `make` that reads the failure is typed only inline. Annotate the parameter as `HttpApiError.HttpApiSchemaError` (type import from `effect/http-api`), or annotate the constant as `ActionGroup.SchemaErrorPolicy<typeof Invalid, typeof Internal>`.
|
|
126
|
+
- `Missing handlers for group "<group>": <actions>` thrown at `implement`, or failing the layer build: the handler record has no own-property function for those actions. Add them to the record itself, not to a prototype.
|
|
125
127
|
- A service is resolved once and shared across requests when it should be per request: it was yielded in the builder. Move the `yield*` into the handler.
|
|
126
128
|
- `Duplicate contract group` thrown by `contracts`: two groups share a name, so their keys would collide. Rename one.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -23,9 +23,8 @@ Exported types: `Options`, `LayerOptions`, `Http`, `Api`.
|
|
|
23
23
|
| `make`: `errors` | Surface error codecs declared on every endpoint; defaults to none. |
|
|
24
24
|
| `layer`: `before` | Effectful hook receiving the selected `Action.Any`, after successful input decoding and before its handler. Failures must belong to the binding's `errors`. |
|
|
25
25
|
|
|
26
|
-
Layer failures and startup requirements come from the supplied implementations
|
|
27
|
-
|
|
28
|
-
requirements; router/platform services are also required. No request identity is supplied at startup.
|
|
26
|
+
Layer failures and startup requirements come from the supplied implementations. Every
|
|
27
|
+
handler's and the hook's request services remain router request requirements; router/platform services are also required. No request identity is supplied at startup.
|
|
29
28
|
|
|
30
29
|
Route shape: `POST <apiPath>/<group>/<action>`. Operation ID: `<group>.<action>`.
|
|
31
30
|
|
|
@@ -33,7 +32,7 @@ Route shape: `POST <apiPath>/<group>/<action>`. Operation ID: `<group>.<action>`
|
|
|
33
32
|
|
|
34
33
|
```ts
|
|
35
34
|
import { Layer } from "effect";
|
|
36
|
-
import { HttpApiScalar, HttpApiSwagger } from "effect/
|
|
35
|
+
import { HttpApiScalar, HttpApiSwagger } from "effect/http-api";
|
|
37
36
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
38
37
|
import { PublicActions, UserActions } from "./contracts.js";
|
|
39
38
|
import { PublicApp, UserApp } from "./handlers.js";
|
|
@@ -74,9 +73,9 @@ The client is Effect's native grouped `HttpApiClient` over `Http.api`. Code that
|
|
|
74
73
|
run Effects uses the Promise client in [ActionHttpClient.md](ActionHttpClient.md).
|
|
75
74
|
|
|
76
75
|
```ts
|
|
77
|
-
import { HttpApiClient } from "effect/
|
|
76
|
+
import { HttpApiClient } from "effect/http-api";
|
|
78
77
|
import { Effect } from "effect";
|
|
79
|
-
import { FetchHttpClient } from "effect/
|
|
78
|
+
import { FetchHttpClient } from "effect/http";
|
|
80
79
|
import { Http } from "./quickstart.js";
|
|
81
80
|
|
|
82
81
|
export const greeting = Effect.gen(function* () {
|
|
@@ -88,34 +87,35 @@ export const greeting = Effect.gen(function* () {
|
|
|
88
87
|
|
|
89
88
|
## Rules
|
|
90
89
|
|
|
90
|
+
- HTTP serves every action of every group passed to `make`; an action has no HTTP switch. To keep an action off HTTP, put it in a group left out of `make` and serve that group elsewhere (MCP, Toolkit, CLI).
|
|
91
91
|
- `apiPath` has no default. Group names must be unique within one `make`. Action names need only be unique within their group; equal action names in different groups do not collide.
|
|
92
92
|
- `errors` declares the failures the surface around these endpoints answers with rather than a handler: authentication, authorization, rate limiting, upstream unavailability. They are added to every endpoint's error schemas, so `HttpApiClient`, `ActionCliClient` and the in-memory `Testing.httpClient` decode them as typed failures, and they appear in OpenAPI on every operation. A schema an action already declares is not repeated.
|
|
93
93
|
- Schemas reachable from one endpoint must have distinct `_tag`s: the client decodes a response by trying the schemas declared for its status, and two errors may share a status. Effect unions them per status.
|
|
94
94
|
- `errors` changes only what is declared. Nothing produces them: the middleware that renders those responses must encode a body that matches the schema, or the client sees a decode error again. Handlers cannot fail with them.
|
|
95
|
-
- `Http.layer(apps, options?)` requires each implementation to reference the exact group object passed to `make`; reconstructing an equal-looking group is not sufficient. Each group may occur only once per call. Only supplied implementations are mounted, and a group
|
|
95
|
+
- `Http.layer(apps, options?)` requires each implementation to reference the exact group object passed to `make`; reconstructing an equal-looking group is not sufficient. Each group may occur only once per call. Only supplied implementations are mounted, and a group without actions is not built. Options are optional.
|
|
96
96
|
- `before` runs after successful native input decoding and before the selected handler. Invalid input skips the hook and handler. A refusal uses the binding's declared error schema and `httpApiStatus`; its services join handler request requirements. Admission that must precede decoding belongs in outer native HTTP middleware.
|
|
97
97
|
- Middleware and the hook are per layer call. Groups that need different middleware or a different policy go in separate `Http.layer` calls, merged with `Layer.mergeAll`.
|
|
98
98
|
- `ActionHttp` sets no response headers of its own. The host owns cache policy; `Authentication.middleware` marks its responses `cache-control: no-store`.
|
|
99
99
|
- A hook refusal and a handler error are plain declared errors: a JSON body and a status, no other headers. Challenge headers such as `WWW-Authenticate` belong to the admission middleware that runs before the router reaches these routes (see [Authentication.md](Authentication.md)), which sets them on its own response.
|
|
100
100
|
- Request-time handler services are `HttpRouter.Request.From<"Requires", R>`. Supply them with router middleware (`Authentication.middleware`, `HttpRouter.middleware`), `HttpRouter.provideRequest`, or the request context. Build-time services are ordinary layer requirements.
|
|
101
|
-
- `Http.api` is a plain `HttpApi`. Anything Effect can do with an `HttpApi` works: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `
|
|
101
|
+
- `Http.api` is a plain `HttpApi`. Anything Effect can do with an `HttpApi` works: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `Http.api.addHttpApi(other)` to combine with other APIs, `HttpApiClient.make`.
|
|
102
102
|
- `Http.openApi(path?)` is `OpenApi.fromApi(Http.api)` as one `GET` route, `<apiPath>/openapi.json` unless a path is given. It documents every bound group, not only the served ones. It is a plain route: middleware provided to its layer covers it, and nothing covers it otherwise.
|
|
103
103
|
- Client calls always take `{ payload }`. No-input actions take `{ payload: {} }`. Pass `null` or `undefined` only when the codec accepts it. There is no flat client.
|
|
104
|
-
- Client effects fail with the declared errors, the group's policy errors, the binding's `errors`, `SchemaError` for local codec failures
|
|
104
|
+
- Client effects fail with the declared errors, the group's policy errors, the binding's `errors`, `SchemaError` for local codec failures (including an undeclared input field, before sending), and native `HttpClientError`.
|
|
105
105
|
- Add authentication headers with the native `transformClient` option. Use `HttpApiClient.makeWith` for custom error or service channels. Native per-call response modes are available.
|
|
106
106
|
- Wire format without a policy: input failure is an empty 400, success is the encoded body, a declared error is its JSON encoding with its `httpApiStatus`, an encoding failure is an empty 400, a defect is an empty 500. Full table in [guarantees.md](guarantees.md).
|
|
107
107
|
- Each handler runs in a span named `<group>.<action>`, a child of the request span, attributed with `action.group`, `action.name` and `action.access`; its log lines carry the same annotations. The hook, decoding and encoding are outside it, in the request span.
|
|
108
108
|
|
|
109
109
|
## Failure modes
|
|
110
110
|
|
|
111
|
-
- Route returns 404: the implementation was never passed to `Http.layer`, or
|
|
111
|
+
- Route returns 404: the implementation was never passed to `Http.layer`, or its group is not bound by `make`, or the path lacks the group segment.
|
|
112
112
|
- Type error listing `HttpRouter.Request.From<"Requires", CurrentActor>` as unsatisfied: a handler yields a request service and no middleware provides it. Wrap that `Http.layer` call with the middleware's `.layer`.
|
|
113
113
|
- Two groups share a name: `make` throws. Rename one.
|
|
114
114
|
- `Implementation of group "x" is not served by this adapter`: `Http.layer` received an implementation whose group was not passed to `ActionHttp.make`. Implement the exact shared group object bound by `make`, or add a genuinely new group to the binding. Matching names and schemas do not establish identity.
|
|
115
115
|
- `Duplicate implementation group: <name>`: a single `Http.layer` call received more than one implementation of the same group. Pass one implementation per group.
|
|
116
|
-
- Client method missing for an action:
|
|
117
|
-
- Empty 400 on a valid-looking request: input did not decode. Set a `schemaError` policy on the group to get a typed body
|
|
118
|
-
- 415:
|
|
116
|
+
- Client method missing for an action: its group is not bound by `make`.
|
|
117
|
+
- Empty 400 on a valid-looking request: input did not decode, or it carries a field the action does not declare. Set a `schemaError` policy on the group to get a typed body.
|
|
118
|
+
- 415 `Unsupported content-type: <type>`: a content type other than `application/json`. A missing `Content-Type` is read as JSON.
|
|
119
119
|
- `HttpClientError: Decode error (401 POST ...)` from a typed client: the surface answered with a status no endpoint declares. Add that error schema to `ActionHttp.make`'s `errors`.
|
|
120
120
|
- A surface error decodes as the wrong type: two schemas that share a status also share a `_tag`. Give them distinct tags.
|
|
121
121
|
- The hook's services appear as unsatisfied `HttpRouter.Request.From<"Requires", ...>`: the hook yields an identity tag and this `Http.layer` call has no middleware providing it. Wrap that call with the middleware's `.layer`.
|
package/docs/ActionHttpClient.md
CHANGED
|
@@ -7,17 +7,21 @@ page. It is Effect's native `HttpApiClient` over `fetch`, built once; each call
|
|
|
7
7
|
|
|
8
8
|
Import `@gjermundgaraba/effect-actions/ActionHttpClient`.
|
|
9
9
|
|
|
10
|
-
| API | Purpose
|
|
11
|
-
| ------------------------- |
|
|
12
|
-
| `promise(http, options?)` | `client.<group>.<action>(input)` for every
|
|
13
|
-
| `Options` | Where and how requests are sent.
|
|
14
|
-
| `Client`, `Method` | The client's type, derived from the binding, and one action's method.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
|
10
|
+
| API | Purpose |
|
|
11
|
+
| ------------------------- | --------------------------------------------------------------------- |
|
|
12
|
+
| `promise(http, options?)` | `client.<group>.<action>(input)` for every action of the binding. |
|
|
13
|
+
| `Options` | Where and how requests are sent. |
|
|
14
|
+
| `Client`, `Method` | The client's type, derived from the binding, and one action's method. |
|
|
15
|
+
|
|
16
|
+
The options are the native `HttpApiClient.make` options except `transformResponse`, plus `fetch`.
|
|
17
|
+
`transformResponse` may change a call's success, failure or required services, which neither
|
|
18
|
+
the method types nor a Promise can follow; wrap `fetch` or use `transformClient` instead.
|
|
19
|
+
|
|
20
|
+
| Option | Meaning |
|
|
21
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `baseUrl` | What routes are resolved against, such as `https://api.example.com`. Omitted: relative routes (the page's origin in a browser). |
|
|
23
|
+
| `transformClient` | Wraps the native `HttpClient`. A bearer token: `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. |
|
|
24
|
+
| `fetch` | The transport. Defaults to the global `fetch`, looked up on each call. |
|
|
21
25
|
|
|
22
26
|
A method takes the action's decoded input and resolves with its decoded success. An action
|
|
23
27
|
whose input may be empty (no `input`, or only optional fields) may be called without an argument.
|
|
@@ -25,14 +29,16 @@ whose input may be empty (no `input`, or only optional fields) may be called wit
|
|
|
25
29
|
## Canonical
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
|
-
import { HttpClientError } from "effect/
|
|
32
|
+
import { HttpClient, HttpClientError, HttpClientRequest } from "effect/http";
|
|
29
33
|
import * as ActionHttpClient from "@gjermundgaraba/effect-actions/ActionHttpClient";
|
|
30
34
|
import { Http, UserNotFound } from "./contracts.js";
|
|
31
35
|
|
|
32
36
|
// Promises in, Promises out: for code that does not run Effects, such as a browser page.
|
|
37
|
+
// The options are the native `HttpApiClient.make` options plus `fetch`; a bearer token is
|
|
38
|
+
// a `transformClient`.
|
|
33
39
|
const client = ActionHttpClient.promise(Http, {
|
|
34
40
|
baseUrl: "http://127.0.0.1:3000",
|
|
35
|
-
|
|
41
|
+
transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken("alice")),
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
export const userName = async (id: string): Promise<string> => {
|
|
@@ -52,22 +58,23 @@ export const userName = async (id: string): Promise<string> => {
|
|
|
52
58
|
};
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
Other headers
|
|
56
|
-
|
|
61
|
+
Other headers also go through `transformClient` (`HttpClient.mapRequest(HttpClientRequest.setHeader("x-agent", agent))`).
|
|
62
|
+
Reacting to a response every call may meet (a proxy's 401), or a token read per call, goes through
|
|
63
|
+
`fetch`: `fetch: (input, init) => { const request = new Request(input, init); request.headers.set("authorization", "Bearer " + readToken()); return fetch(request); }`.
|
|
57
64
|
|
|
58
65
|
## Rules
|
|
59
66
|
|
|
60
67
|
- A call rejects with exactly what the native client's Effect fails with. Declared errors arrive as their decoded values: the action's own (its group's `errors` included), its group's schema-error policy's, and the binding's surface `errors`. Classify them with `instanceof`, their `_tag`, or `Schema.is`.
|
|
61
|
-
- Anything the contract does not account for rejects with Effect's own error. `HttpClientError`: the server could not be reached (`response` is `undefined`), or answered with a status no schema declares, or with a body that could not be read. `SchemaError`: the input did not encode, or the success body did not decode.
|
|
68
|
+
- Anything the contract does not account for rejects with Effect's own error. `HttpClientError`: the server could not be reached (`response` is `undefined`), or answered with a status no schema declares, or with a body that could not be read. `SchemaError`: the input did not encode (including a field the action does not declare; nothing is sent), or the success body did not decode.
|
|
62
69
|
- The library adds no error type of its own and interprets no status. Which failures mean "signed out" or "try again" is the caller's decision.
|
|
63
70
|
- Nothing is retried. A rejected write may or may not have happened; only a declared error says what the server did.
|
|
64
|
-
-
|
|
71
|
+
- Every action of a bound group has a method. A group without actions has no namespace.
|
|
65
72
|
- The client holds no connections or timers. Making one is synchronous and sends nothing.
|
|
66
73
|
- A method is not an Effect. Code that runs Effects uses `HttpApiClient.make(Http.api)` directly (see [ActionHttp.md](ActionHttp.md)); its methods take `{ payload }`.
|
|
67
74
|
|
|
68
75
|
## Failure modes
|
|
69
76
|
|
|
70
|
-
- Rejects with `HttpClientError` whose `reason._tag` is `
|
|
77
|
+
- Rejects with `HttpClientError` whose `reason._tag` is `DecodeError` (`Decode error (401 POST ...)`) for a 401 or 403: the surface answered with an error the binding does not declare. Add it to `ActionHttp.make`'s `errors`, and have the middleware encode it.
|
|
71
78
|
- Rejects with `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: `baseUrl` is omitted, and there is no page to resolve relative routes against. Set `baseUrl`.
|
|
72
79
|
- A stubbed global `fetch` is not used: `fetch` was passed as an option, which takes precedence.
|
|
73
|
-
- Property does not exist on the client: the action
|
|
80
|
+
- Property does not exist on the client: the action's group is not bound in this `Http`.
|
package/docs/ActionMcp.md
CHANGED
|
@@ -74,7 +74,7 @@ choose its policy for every route it covers.
|
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
import { Effect, Layer } from "effect";
|
|
77
|
-
import { HttpRouter } from "effect/
|
|
77
|
+
import { HttpRouter } from "effect/http";
|
|
78
78
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
79
79
|
import { Actions } from "./quickstart.js";
|
|
80
80
|
|
|
@@ -142,14 +142,16 @@ Layer.launch(layer).pipe(
|
|
|
142
142
|
|
|
143
143
|
## Rules
|
|
144
144
|
|
|
145
|
-
- Both transports serve MCP 2026-07-28 and no other revision; there is no `protocols` option. Over HTTP the endpoint is stateless: no initialize handshake, no session, and every request stands alone. Effect owns version checks and rejects any other revision.
|
|
146
|
-
- `path` has no default. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form
|
|
145
|
+
- Both transports serve MCP 2026-07-28 and no other revision; there is no `protocols` option. Over HTTP the endpoint is stateless: no initialize handshake, no session, and every request stands alone. Effect owns version checks and rejects any other revision. Stdio has no sessions, so it could serve an older host, but it refuses one deliberately, for uniformity: a client that works over one transport works over the other.
|
|
146
|
+
- `path` has no default. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form.
|
|
147
147
|
- 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. Authentication middleware wrapping the endpoint runs first and may reject the request before native Origin validation; the allowlist does not protect authentication from untrusted-origin requests.
|
|
148
148
|
- `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.
|
|
149
149
|
- An endpoint is one route. Middleware provided to `layerHttp` covers all of its tools. To serve tools under different middleware, mount them on different paths with separate `layerHttp` calls.
|
|
150
|
-
- Only MCP-enabled actions become tools, under `mcp.name` with the resolved hints. A group with no tools is not built.
|
|
150
|
+
- Only MCP-enabled actions become tools, under `mcp.name` with the resolved hints. A group with no tools is not built. The layer's types drop an action's requirements only when its type is certainly hidden (a literal `mcp: false`, see [Action.md](Action.md)); an `mcp` that may be `false` at runtime keeps them.
|
|
151
151
|
- Every MCP-enabled action must have object-root input; the native server refuses anything else when the layer is built. Omit `input` for a tool with no arguments.
|
|
152
|
-
- Success is `structuredContent:
|
|
152
|
+
- Success is `structuredContent: <encoded success>`, of any JSON type, and one text block holding the same JSON, as the specification recommends. The tool's `outputSchema` describes the encoded success. 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`.
|
|
153
|
+
- With a text field (`mcp.text`), a success is `content: [<field>, <JSON of the rest>]` and `structuredContent: <the rest>`. The field is sent once, verbatim, and is left out of `structuredContent` and of the listed `outputSchema`. Use it for a body the model should read raw, such as a page of Markdown: in structured content it would be JSON-escaped, and sent twice. The field must be a top-level property of the success's JSON Schema. A success without the field as a string, such as one that omits an optional field, is sent whole.
|
|
154
|
+
- Besides `content`, `structuredContent` and `isError: false`, the server adds `_meta["io.modelcontextprotocol/serverInfo"]` and `resultType: "complete"` to every result. `serverInfo` is the layer's `name`, `version`, `description`, `websiteUrl` and `icons`, as given, so a result's encoded size is the size of its own fields plus a fixed overhead per endpoint, the same over HTTP and stdio.
|
|
153
155
|
- Invalid arguments and unencodable results are answered by the native `McpServer`: an `isError` result with a message for the model. Group `schemaError` policies do not apply.
|
|
154
156
|
- Defects and encoding failures produce the generic `isError` text `Tool execution failed due to an internal server error.`; the cause is logged, not sent.
|
|
155
157
|
- Tool discovery is not filtered by actor. Every caller sees every tool of the endpoint. Authorization happens in `before`.
|
|
@@ -163,7 +165,7 @@ Layer.launch(layer).pipe(
|
|
|
163
165
|
## Failure modes
|
|
164
166
|
|
|
165
167
|
- Layer build dies while registering tools, with a defect whose `SchemaError` message says `Expected "object"` or `Missing key`: an MCP-enabled action has scalar, array, or empty-struct input, which the native server refuses. It is not in the layer's error channel, so it cannot be caught by tag. Wrap the input in a struct with at least one field, omit `input` for no arguments, or set `mcp: false`.
|
|
166
|
-
- `Duplicate MCP tool: <name>` thrown at the `layerHttp` or `layerStdio` call: two
|
|
168
|
+
- `Duplicate MCP tool: <name>` thrown at the `layerHttp` or `layerStdio` call: two actions on one endpoint expose the same tool name. Rename with `mcp.name` or split the endpoint.
|
|
167
169
|
- Type error listing `HttpRouter.Request.From<"Requires", ...>`: a handler yields a request service and the endpoint has no middleware providing it. Provide it with `Layer.provide(middleware.layer)` on that `layerHttp`.
|
|
168
170
|
- Public tool requires a token: it shares an endpoint with protected tools. Give it its own path.
|
|
169
171
|
- Client reports a broken transport from a stdio subprocess: something printed to stdout. Set `Logger.LogToStderr` and remove `console.log`.
|
|
@@ -172,4 +174,5 @@ Layer.launch(layer).pipe(
|
|
|
172
174
|
- A disallowed Origin receives 401 instead: wrapping authentication rejected it before the native Origin check. Put any required pre-authentication Host/Origin policy in outer host middleware.
|
|
173
175
|
- 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.
|
|
174
176
|
- `Object literal may only specify known properties, and 'protocols'`: the revision is fixed. Delete the option.
|
|
177
|
+
- Layer build dies with `MCP tool '<name>' cannot send '<field>' as text: it is not a top-level property of its success`: the success's JSON Schema has no such top-level property, as for a union of structs. Make the success one struct.
|
|
175
178
|
- A refusal arrives as a generic internal-error result: the hook failed with an error this transport does not declare, which is a defect. Add its schema to `errors`.
|
package/docs/ActionToolkit.md
CHANGED
|
@@ -51,8 +51,8 @@ With a model: pass `binding.toolkit` as `toolkit` to `LanguageModel.generateText
|
|
|
51
51
|
|
|
52
52
|
## Rules
|
|
53
53
|
|
|
54
|
-
- Selects MCP-enabled actions only, named by `mcp.name`, with resolved hints. Groups with no selected tools are not built. `make(apps, options?)` accepts a readonly implementation collection; options may be omitted.
|
|
55
|
-
- Successes are the action's native values
|
|
54
|
+
- Selects MCP-enabled actions only, named by `mcp.name`, with resolved hints. Groups with no selected tools are not built. The toolkit's type follows `ActionMcp`'s rule: only an action whose `mcp` type is exactly `false` has no tool; one whose `mcp` may be `false` at runtime is typed as a tool with its handler's requirements. `make(apps, options?)` accepts a readonly implementation collection; options may be omitted.
|
|
55
|
+
- Successes are the action's native values, whole: `mcp.text` does not apply. Declared failures are returned as native tool results (`failureMode: "return"`), not raised.
|
|
56
56
|
- `tools.handle(name, encodedInput)` takes encoded arguments and returns an Effect producing a result stream. The handler starts while that stream is constructed, so request services must be provided around the entire `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, not only around the stream.
|
|
57
57
|
- `binding.layer` acquires each implementation once, in the layer's scope. Build-time requirements belong to the whole selected implementation.
|
|
58
58
|
- Each tool carries only its own handler's request requirements, plus the hook's, not those of sibling or disabled tools.
|
|
@@ -65,7 +65,7 @@ With a model: pass `binding.toolkit` as `toolkit` to `LanguageModel.generateText
|
|
|
65
65
|
|
|
66
66
|
- Tool missing from the toolkit: the action has `mcp: false`. The Toolkit selects by MCP enablement.
|
|
67
67
|
- Type error on `before`'s error channel: it fails with an error this binding does not declare. Add its schema to `errors`.
|
|
68
|
-
- `Duplicate MCP tool: <name>` thrown at `make`: two
|
|
69
|
-
- `Service not found` for a request tag at call time: it was provided only to the stream
|
|
68
|
+
- `Duplicate MCP tool: <name>` thrown at `make`: two selected actions expose the same tool name. Rename with `mcp.name`.
|
|
69
|
+
- `Service not found` for a request tag at call time: it was provided only to the stream. Provide it around the whole call effect. Providing it to `binding.layer` does not fail: the startup value is captured and served to every call (see [guarantees.md](guarantees.md)).
|
|
70
|
+
- A returned failure that is an `AiError` with reason `ToolParameterValidationError`, not a declared error: the arguments did not decode. The hook and the handler did not run.
|
|
70
71
|
- Type error on `binding.layer` requirements: a build-time service is missing. Provide its Layer before `binding.layer`.
|
|
71
|
-
- Result is `{ value: ... }` when a plain value was expected: you are reading an MCP response, not a Toolkit result. The Toolkit never wraps.
|
package/docs/Authentication.md
CHANGED
|
@@ -29,7 +29,7 @@ to routes requiring that identity.
|
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
31
|
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
32
|
-
import { HttpServerRequest, HttpServerResponse } from "effect/
|
|
32
|
+
import { HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
33
33
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
34
34
|
|
|
35
35
|
interface Actor {
|
|
@@ -84,7 +84,7 @@ export const routes = Layer.mergeAll(
|
|
|
84
84
|
- Downstream action errors are handled by their transport. They are never serialized as authentication failures.
|
|
85
85
|
- Use distinct tags for startup capabilities and request identities. Never provide `CurrentActor` or any identity or tenant tag in a startup layer or root context. Native context capture can let a startup value shadow the request value or satisfy a missing one, and the adapters add no isolation boundary. Types track that the tag is required, not where its value came from.
|
|
86
86
|
- `protectedResource` publishes what it is given. The deployment must ensure `resource` and `authorizationServers` are valid OAuth URLs (HTTPS, or loopback HTTP in development).
|
|
87
|
-
- Discovery is served at `/.well-known/oauth-protected-resource` followed by the resource's path and query, for `GET` and `HEAD`, matching that literal path and query. Other requests fall through to the host router.
|
|
87
|
+
- Discovery is served at `/.well-known/oauth-protected-resource` followed by the resource's path and query, for `GET` and `HEAD`, matching that literal path and query. Other requests fall through to the host router. `discovery.layer` is global router middleware: it answers before any route middleware, so authentication never covers it. Caching policy is the host's.
|
|
88
88
|
- `challenge()` quotes and escapes parameter values; it never rejects them. Name only the scopes needed for the request in `scope`; `scopesSupported` advertises the full set in metadata.
|
|
89
89
|
- Tool discovery is never filtered by actor. Authorization belongs in each surface's `before` hook, which runs with the action contract in hand; write the rule against `action.access` rather than repeating a check in each handler.
|
|
90
90
|
- A response this middleware renders is not part of any endpoint's contract. Declare its schema in `ActionHttp.make`'s `errors` so typed clients decode the 401 instead of reporting a decode error ([ActionHttp.md](ActionHttp.md)).
|
|
@@ -94,6 +94,5 @@ export const routes = Layer.mergeAll(
|
|
|
94
94
|
- Handler sees a stale or wrong actor: an identity tag was provided at startup. Remove it from every startup layer; provide it only through the middleware.
|
|
95
95
|
- Type error `HttpRouter.Request.From<"Requires", CurrentActor>` unsatisfied: the layer serving that implementation was not wrapped with `authentication.layer`.
|
|
96
96
|
- Discovery returns 404: the request path does not match `resource`'s path and query exactly, or `discovery.layer` is not merged into the served layer.
|
|
97
|
-
- Discovery requires a token: `discovery.layer` was placed under the authentication middleware. Mount it separately.
|
|
98
97
|
- 401 response lacks `WWW-Authenticate`: the host's failure response did not set it. Use `discovery.challenge(...)` in its headers.
|
|
99
98
|
- A typed client reports `Decode error (401 ...)` instead of the failure schema: the response this middleware renders is not declared. Add it to `ActionHttp.make`'s `errors`.
|
package/docs/CONTEXT.md
CHANGED
|
@@ -5,7 +5,7 @@ use these words with these meanings.
|
|
|
5
5
|
|
|
6
6
|
## Terms
|
|
7
7
|
|
|
8
|
-
- **Action**: one contract. A name, a description, an input schema, a success schema, declared error schemas, authorization metadata (`access`), and
|
|
8
|
+
- **Action**: one contract. A name, a description, an input schema, a success schema, declared error schemas, authorization metadata (`access`), and MCP metadata (`mcp`). Holds no behavior. Made by `Action.make`.
|
|
9
9
|
- **Access**: `"read"` or `"write"`, required on every action and kept as a literal. States what the action does to its resource. Authorization metadata read by a pre-handler hook, independent of the MCP `readOnly` hint, which defaults from it.
|
|
10
10
|
- **Group**: a named, ordered set of actions plus group-level errors and an optional schema-error policy. Adapters serve groups, clients are organized by group, and the group name is the OpenAPI tag. Made by `ActionGroup.make`.
|
|
11
11
|
- **Contract**: an action or a group. Pure data; safe to import anywhere, including browsers.
|
|
@@ -16,26 +16,26 @@ use these words with these meanings.
|
|
|
16
16
|
- **Build-time requirement**: a service yielded in the builder. Resolved when an adapter layer is built.
|
|
17
17
|
- **Request-time requirement**: a service yielded inside a handler. Supplied per invocation: by router middleware for HTTP-hosted adapters, by the host for Toolkit, CLI, and stdio.
|
|
18
18
|
- **Adapter** (also **projection**): a module that maps implementations or contracts onto one surface: `ActionHttp`, `ActionMcp`, `ActionToolkit`, `ActionCli`, `ActionCliClient`, `ActionCatalog`. Each adapter reads only what it serves and validates only its own namespace.
|
|
19
|
-
- **Binding**: the value an adapter returns before anything runs. `ActionHttp.make` returns the HTTP binding (`groups`, `api`, `layer`); `ActionToolkit.make` returns the Toolkit binding (`toolkit`, `layer`).
|
|
19
|
+
- **Binding**: the value an adapter returns before anything runs. `ActionHttp.make` returns the HTTP binding (`groups`, `api`, `layer`, `openApi`); `ActionToolkit.make` returns the Toolkit binding (`toolkit`, `layer`).
|
|
20
20
|
- **Contract map**: the record `ActionGroup.contracts` returns, keyed `<group>.<action>`, with each action's exact type. The shared identity of an action across routes, tools and commands.
|
|
21
21
|
- **Declared error**: an error schema listed on an action or inherited from its group. Handlers may fail with it. HTTP encodes it with its `httpApiStatus`; MCP returns encoded error text in an `isError` result. Toolkit returns a native failure result, and CLI fails its command Effect without serializing the error.
|
|
22
22
|
- **Schema-error policy**: a group option answering Effect's `HttpApiSchemaError` with a declared policy error and its status: `invalid` when the request did not decode, `internal` when the handler's result did not encode. The library decides which applies. HTTP only.
|
|
23
23
|
- **Policy error**: an error a schema-error policy may answer with. Part of the transport contract, never returnable by a handler.
|
|
24
24
|
- **Surface error**: an error declared on an adapter binding rather than on an action. Produced by middleware around the surface or by its pre-handler hook, never by a handler; declared so typed callers decode it.
|
|
25
|
-
- **Local-only action**: `http: false` and `mcp: false`. Reachable through `ActionCli` and listed in the catalog.
|
|
26
25
|
- **Tool**: the MCP or Toolkit projection of an MCP-enabled action, named by `mcp.name` and carrying its hints.
|
|
26
|
+
- **Text field**: the string field of an action's encoded success that `mcp.text` names. When a success has it, MCP sends it once, raw, as a text block, and leaves it out of the structured content. Every other surface serves the whole success.
|
|
27
27
|
- **Endpoint**: one `ActionMcp.layerHttp` mount. One route, one middleware set, one tool registry.
|
|
28
28
|
- **Document**: the OpenAPI output of `Http.api`, produced by Effect (`OpenApi.fromApi`) and served by `Http.openApi`.
|
|
29
|
-
- **Promise client**: `ActionHttpClient.promise(Http)`, the native `HttpApiClient` as one Promise method per
|
|
29
|
+
- **Promise client**: `ActionHttpClient.promise(Http)`, the native `HttpApiClient` as one Promise method per action of the binding, for code that does not run Effects. It rejects with what the native client fails with.
|
|
30
30
|
- **Catalog**: the offline JSON description of groups produced by `ActionCatalog.make`, with standalone JSON Schemas for encoded values. Descriptive only.
|
|
31
31
|
- **Identity**: the per-request principal, provided by `Authentication.middleware` under an application-owned tag. Never provided at startup.
|
|
32
32
|
|
|
33
33
|
## Seams
|
|
34
34
|
|
|
35
|
-
- `src/Action.ts` and `src/ActionGroup.ts` are contracts. They import
|
|
35
|
+
- `src/Action.ts` and `src/ActionGroup.ts` are contracts. They import no transport runtime. They carry only transport metadata (`mcp`, `httpApiStatus`, the HTTP schema-error policy).
|
|
36
36
|
- `src/internal/actions.ts` and `src/internal/implementation.ts` hold the shared shapes adapters consume: the contract half of a group, the schema-error policy, and the nominal `Implementation` class. Everything under `src/internal` is not public API; the public modules re-export the types consumers need.
|
|
37
37
|
- Each adapter owns its own mapping from contracts to its surface and never reaches into another adapter. HTTP does not know about tool names; MCP does not know about routes.
|
|
38
|
-
- Adapters build on Effect's own servers and clients: `HttpApi` for HTTP, `McpServer` for MCP, `Toolkit` for the AI toolkit, `Command` for the CLI. The library adds no protocol runtime
|
|
38
|
+
- Adapters build on Effect's own servers and clients: `HttpApi` for HTTP, `McpServer` for MCP, `Toolkit` for the AI toolkit, `Command` for the CLI. The library adds no protocol runtime.
|
|
39
39
|
- Authentication is router middleware plus discovery metadata. Verification, login, and consent stay in the application. Authorization is the application's pre-handler hook, bound to each surface, which the library runs but never writes.
|
|
40
40
|
- `Testing` depends only on `effect`. `TestingClient` is the single module that loads the optional MCP client peer.
|
|
41
41
|
- Docs: `README.md` is the pitch for humans. `docs/` is the reference for agents, one card per module. `skills/effect-actions` is a copy of `docs/`, with this vocabulary included. `CONTRIBUTING.md` and `AGENTS.md` are for maintainers.
|
package/docs/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# effect-actions reference
|
|
2
2
|
|
|
3
|
-
Reference for `@gjermundgaraba/effect-actions`, written for coding agents. Every page is a
|
|
3
|
+
Reference for `@gjermundgaraba/effect-actions`, written for coding agents. Every module page is a
|
|
4
4
|
card with the same sections: **API** (inventory and options), **Canonical** (the one right way to write
|
|
5
5
|
it), **Rules** (must and never), **Failure modes** (what you see when it is wrong, and the fix).
|
|
6
|
+
[setup.md](setup.md), [guarantees.md](guarantees.md), and [CONTEXT.md](CONTEXT.md) are cross-cutting pages with their own structure.
|
|
6
7
|
Read the card for a module before writing code that uses it. Exported TypeScript declarations are the exact signature reference. Vocabulary is defined in
|
|
7
8
|
[CONTEXT.md](CONTEXT.md); the pages use those terms exactly.
|
|
8
9
|
|
|
9
10
|
Package facts that apply everywhere:
|
|
10
11
|
|
|
11
12
|
- Every module is a subpath import: `import * as Action from "@gjermundgaraba/effect-actions/Action"`. There is no package root.
|
|
12
|
-
- The `effect` peer accepts any 4.0 release candidate from `4.0.0-rc.
|
|
13
|
+
- The `effect` peer accepts any 4.0 release candidate from `4.0.0-rc.118` on (`>=4.0.0-rc.118 <4.0.0`). The package is built and tested against `4.0.0-rc.118`.
|
|
13
14
|
- Contracts are pure values. Nothing runs, binds, or acquires services until an adapter layer is built.
|
|
14
15
|
|
|
15
16
|
## Pages
|
|
@@ -83,9 +84,8 @@ Repository directory `examples/` ([on GitHub](https://github.com/gjermundgaraba/
|
|
|
83
84
|
|
|
84
85
|
- `quickstart.ts`, `quickstart-client.ts`: the minimal program and its typed client.
|
|
85
86
|
- `promise-client.ts`: the Promise client, with a declared error and an unreachable server.
|
|
86
|
-
- `contracts.ts`, `handlers.ts`, `app.ts`, `server.ts`: an authenticated application with three groups, per-group middleware, two MCP endpoints, OpenAPI and Swagger.
|
|
87
|
+
- `contracts.ts`, `auth.ts`, `users.ts`, `handlers.ts`, `app.ts`, `server.ts`, `client.ts`: an authenticated application with three groups, per-group middleware, two MCP endpoints, OpenAPI and Swagger.
|
|
87
88
|
- `toolkit.ts`, `catalog.ts`, `cli.ts`, `cli-client.ts`, `mcp-stdio.ts`: one file per other projection.
|
|
88
|
-
|
|
89
89
|
- `mcp-browser.ts`: a stateless MCP endpoint with a separate browser CORS policy.
|
|
90
90
|
- `toolkit-authorized.ts`: native Toolkit invocation with authorization and correctly scoped identity.
|
|
91
91
|
- `testing.ts`: in-memory HTTP and MCP calls with cleanup.
|
package/docs/Testing.md
CHANGED
|
@@ -16,7 +16,7 @@ Import `@gjermundgaraba/effect-actions/Testing`; official-client helpers are in
|
|
|
16
16
|
| `TestingClient.withMcpClient(options, run)` | Connect the official client, run an asynchronous callback, then close it. |
|
|
17
17
|
|
|
18
18
|
`httpClient` takes native client options; `baseUrl` defaults to `http://localhost`.
|
|
19
|
-
Its exported `Handler` type is a web request to response Promise.
|
|
19
|
+
Its exported `Handler` type is a web request to response Promise, not an action handler.
|
|
20
20
|
|
|
21
21
|
| MCP option | Meaning |
|
|
22
22
|
| ------------------------------------- | ----------------------------------------------------------- |
|
|
@@ -34,7 +34,7 @@ Exported option/value types: `McpRequestOptions`, `McpRequestParams`, `McpReques
|
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
import { Effect, Layer } from "effect";
|
|
37
|
-
import { HttpRouter, HttpServer } from "effect/
|
|
37
|
+
import { HttpRouter, HttpServer } from "effect/http";
|
|
38
38
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
39
39
|
import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
|
|
40
40
|
import { Http, routes } from "./quickstart.js";
|
|
@@ -78,17 +78,20 @@ try {
|
|
|
78
78
|
- `httpClient` is the same `HttpApiClient` consumers use, with its transport replaced by `handler`. Same `{ payload }` calls, same error channel.
|
|
79
79
|
- `mcpRequest` pins protocol version 2026-07-28 in both the header and `_meta`. Caller `params._meta` fields override the defaulted client capabilities and info; the merge is shallow. Application metadata is preserved.
|
|
80
80
|
- `params` accepts what `JSON.stringify` accepts, so tests can send malformed arguments on purpose. `undefined` fields are dropped.
|
|
81
|
-
- `mcpCall` sends one `tools/call` through `mcpRequest` and resolves with `{ isError: false, value }`, the success taken from `structuredContent
|
|
81
|
+
- `mcpCall` sends one `tools/call` through `mcpRequest` and resolves with `{ isError: false, value }`, the success taken from `structuredContent`, with `text` holding the text field when a success with `mcp.text` sent one, or `{ isError: true, error }`, the result's error text parsed as JSON. A declared error or a hook refusal is its JSON encoding, the same body HTTP sends; the native server's own messages (invalid arguments, defects) are not JSON and stay text. It reads a JSON or an event-stream response.
|
|
82
|
+
- To assert on a result's exact bytes, such as a size bound, send `mcpRequest` through the handler and parse the reply: `JSON.stringify` of the parsed message reproduces the message the server wrote, so `JSON.stringify(message.result)` is the result as encoded.
|
|
82
83
|
- `mcpCall` throws when the answer is not a tool result: a status other than 200 (an authentication refusal), or a JSON-RPC error (an unknown tool). Use `mcpRequest` to assert on those responses.
|
|
83
84
|
- `withMcpClient` pins the official client to MCP 2026-07-28, the only revision `ActionMcp` serves, and closes the transport in a `finally` block.
|
|
84
85
|
- Add `Authorization` through `headers` in the MCP helpers, or through `transformClient` in `httpClient` options.
|
|
85
|
-
- Direct handler tests can use `app.build` under `Effect.scoped`, but they bypass decoding, encoding, and
|
|
86
|
+
- Direct handler tests can use `app.build` under `Effect.scoped`, but they bypass decoding, encoding, middleware, and the `before` hook. Keep at least one adapter-level test per transport.
|
|
86
87
|
|
|
87
88
|
## Failure modes
|
|
88
89
|
|
|
89
90
|
- `@modelcontextprotocol/client` not found: only `TestingClient` needs it. Install the peer in devDependencies, or use `Testing.mcpRequest` instead.
|
|
90
|
-
- `
|
|
91
|
+
- Type error on `versionNegotiation` in `withMcpClient` options: the revision is fixed at 2026-07-28. Delete the option.
|
|
91
92
|
- `MCP tools/call "<name>" answered 401`: the call reached authentication without a credential. Pass `headers: { authorization: "Bearer ..." }`.
|
|
93
|
+
- `MCP tools/call "<name>" failed with <code>: <message>`: a JSON-RPC error, such as an unknown tool. Assert on it with `mcpRequest`.
|
|
94
|
+
- `MCP tools/call "<name>" had no reply` or `returned no structured content`: the response was not a tool result. Inspect it with `mcpRequest`.
|
|
92
95
|
- Handler leaks between tests: `web.dispose()` was not called. Register it with the test runner's cleanup hook.
|
|
93
96
|
- 404 from `httpClient`: the requested route was not mounted, or the path/base URL is wrong. Include `Http.layer([app])` in the served routes.
|
|
94
97
|
- Missing platform-service requirements when constructing the web handler: provide `HttpServer.layerServices` to the routes before `toWebHandler`.
|
package/docs/guarantees.md
CHANGED
|
@@ -24,7 +24,7 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
24
24
|
| Case | HTTP | MCP |
|
|
25
25
|
| --------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
26
26
|
| Input | decoded with the schema's JSON codec; failure is an empty **400** | decoded with the schema's JSON codec; failure is `InvalidParams`, presented as an `isError` result |
|
|
27
|
-
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent:
|
|
27
|
+
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: <encoded>`, and its JSON as text content; a text field is sent as text instead |
|
|
28
28
|
| Declared error | encoded by its schema with its `httpApiStatus` (unannotated: 500): `{"_tag":"UserNotFound","id":"x"}` | `isError: true`; text content is the same JSON; no `structuredContent` |
|
|
29
29
|
| Pre-handler hook failure | the same, using the hook's error schema, after input decoding; the handler never runs | the same `isError` result, after arguments are decoded; the handler never runs |
|
|
30
30
|
| Invalid output encoding | empty **400** (`HttpApiSchemaError`) | `isError: true`, text `Tool execution failed due to an internal server error.`; cause logged, not sent |
|
|
@@ -32,17 +32,18 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
32
32
|
| Unknown path or method | 404 | n/a |
|
|
33
33
|
| Invalid JSON / content type | 400 / 415 | n/a |
|
|
34
34
|
|
|
35
|
+
- `mcp.text` changes only the MCP success result. HTTP, OpenAPI, typed clients, the Toolkit, the CLI and its client, and the catalog serve and describe the whole success. A text field's other outcomes are answered as in this table.
|
|
35
36
|
- A group `schemaError` policy replaces the empty 400s on HTTP with a declared error and status: its `invalid` error for input that does not decode, its `internal` error for output that does not encode. It never affects MCP. Details in [ActionGroup.md](ActionGroup.md).
|
|
36
37
|
- `ActionHttp` sets no headers of its own. The host owns cache and challenge headers; `Authentication.middleware` marks its responses `cache-control: no-store`. Hook refusals and handler failures are ordinary declared error responses.
|
|
37
38
|
- Routes are `POST <apiPath>/<group>/<action>`. Operation IDs are `<group>.<action>`. Effect generates OpenAPI component names and references.
|
|
38
39
|
- `ActionHttp.make`'s `errors` are declared on every endpoint so clients decode the surface's own failures (401, 403, 429, 503). They are produced by middleware or by the binding's hook, never by a handler. `ActionMcp` and `ActionToolkit` declare their own `errors` the same way, joined into every tool's failure schema. Two errors may share an HTTP status; their `_tag`s must differ, because the client decodes a status by trying the schemas declared for it.
|
|
39
|
-
-
|
|
40
|
+
- Input is closed. Both transports publish `additionalProperties: false` for input (so does the catalog) and refuse undeclared fields: over HTTP an extra field is an input failure (an empty 400, or the group's `schemaError` `invalid` answer); an MCP tool (`Tool.Strict`) returns an invalid-arguments result. Typed HTTP clients encode input from `Http.api` and fail with `SchemaError` on an undeclared field, before sending. The native Toolkit, `ActionCli` and `ActionCliClient` decode input locally with default options, which drop an undeclared field instead of refusing it.
|
|
40
41
|
|
|
41
42
|
## Namespaces
|
|
42
43
|
|
|
43
|
-
- Group names are unique within one `ActionHttp.make`. Action names are unique within a group. MCP tool names are unique within
|
|
44
|
+
- Group names are unique within one `ActionHttp.make`. Action names are unique within a group. MCP tool names are unique within each Toolkit or MCP projection that serves them; a group does not check them.
|
|
44
45
|
- Adapters validate only the namespace they serve. HTTP does not check tool names; MCP does not check route names.
|
|
45
|
-
- Each adapter builds only what it serves.
|
|
46
|
+
- Each adapter builds only what it serves. HTTP serves every action of a group it binds, so `Http.layer` builds every implementation it is given (except a group without actions); a group with no tools is not built by MCP or Toolkit.
|
|
46
47
|
|
|
47
48
|
## Observability
|
|
48
49
|
|