@gjermundgaraba/effect-actions 0.4.0 → 0.4.1
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/dist/Action.mjs +1 -0
- package/dist/ActionCli.d.mts +3 -1
- package/docs/Action.md +2 -2
- package/docs/ActionHttp.md +1 -1
- package/docs/ActionMcp.md +2 -0
- package/docs/guarantees.md +1 -1
- package/package.json +1 -1
package/dist/Action.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const NoInput = Schema.Record(Schema.String, Schema.Never);
|
|
|
6
6
|
function make(name, options) {
|
|
7
7
|
assertName("action name", name);
|
|
8
8
|
const { access } = options;
|
|
9
|
+
if (access !== "read" && access !== "write") throw new Error(`Invalid access: ${String(access)}`);
|
|
9
10
|
const readOnly = options.mcp === false ? false : options.mcp?.readOnly ?? access === "read";
|
|
10
11
|
const mcp = options.mcp === false ? false : {
|
|
11
12
|
name: options.mcp?.name ?? name,
|
package/dist/ActionCli.d.mts
CHANGED
|
@@ -28,7 +28,9 @@ type BoundHandler<Name extends string, A extends Any, R> = Readonly<Record<Name,
|
|
|
28
28
|
* Project one explicitly selected local action into a native Effect CLI command.
|
|
29
29
|
* The action name resolves against the bound group; no HTTP fallback exists.
|
|
30
30
|
*/
|
|
31
|
-
export declare const command: <G extends Actions, Name extends G["actions"][number]["name"], H extends BoundHandler<Name, Selected<G, Name>, HandlerContext<H, Name>>, EX, RX, EB = never, RB = never, Parameters extends Command.Command.Config = never>(app: Implementation<G, H, EX, RX>, name: Name, options?: Options<Selected<G, Name>["success"]["Type"], EB, RB, Parameters>) => Command.Command<string, never, {},
|
|
31
|
+
export declare const command: <G extends Actions, Name extends G["actions"][number]["name"], H extends BoundHandler<Name, Selected<G, Name>, HandlerContext<H, Name>>, EX, RX, EB = never, RB = never, Parameters extends Command.Command.Config = never>(app: Implementation<G, H, EX, RX>, name: Name, options?: Options<Selected<G, Name>["success"]["Type"], EB, RB, Parameters>) => Command.Command<string, never, {}, EB | EX | import("effect/Schema").SchemaError | Extract<G["actions"][number], {
|
|
32
|
+
readonly name: Name;
|
|
33
|
+
}>["errors"][number]["Type"], Exclude<RB, Scope.Scope> | Exclude<RX, Scope.Scope> | Exclude<HandlerContext<H, Name>, Scope.Scope>>;
|
|
32
34
|
/** Project every local action below its group namespace with default CLI options. */
|
|
33
35
|
export declare const group: <G extends Actions, H extends Handlers<HandlersContext<H>>, EX, RX, EB = never, RB = never>(app: Implementation<G, H, EX, RX>, options?: GroupOptions<EB, RB>) => Command.Command<string, {}, {}, EB | EX | import("effect/Schema").SchemaError | G["actions"][number]["errors"][number]["Type"], Exclude<Exclude<RB, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<RX, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<HandlersContext<H>, Scope.Scope>, Command.CommandContext<string>>>;
|
|
34
36
|
//#endregion
|
package/docs/Action.md
CHANGED
|
@@ -120,7 +120,7 @@ export const Double = Action.make("double", {
|
|
|
120
120
|
- Omit `input` for a no-argument action. The default is an empty object schema, which satisfies MCP's object-root requirement. Clients still pass `{ payload: {} }`.
|
|
121
121
|
- `errors` is a list of schemas, default none. Each keeps its own `httpApiStatus` annotation; an unannotated error is served as HTTP 500. Group-level `errors` are appended to every action of the group (see [ActionGroup.md](ActionGroup.md)).
|
|
122
122
|
- A handler may fail only with the declared errors. Anything else is a defect.
|
|
123
|
-
- `access` is `"read"` or `"write"` and is required. It stays a literal on the action, so a rule may switch on it at the type level. It is authorization metadata for a surface's `before` hook (see [guarantees.md](guarantees.md)); the library itself authorizes nothing and no adapter reads it.
|
|
123
|
+
- `access` is `"read"` or `"write"` and is required. `make` also checks it at runtime, so a caller the compiler never sees cannot define an action no rule classifies. It stays a literal on the action, so a rule may switch on it at the type level. It is authorization metadata for a surface's `before` hook (see [guarantees.md](guarantees.md)); the library itself authorizes nothing and no adapter reads it.
|
|
124
124
|
- `access` is independent of `mcp`. A local-only action (`mcp: false`) still has one, and an action may set `access: "write"` with `mcp: { readOnly: true }` if the tool hint should say something else. Derive authorization from `access`, never from a tool hint.
|
|
125
125
|
- Both transports are on by default. `http: false` removes the route and the client method. `mcp: false` removes the tool. Both `false` makes a local-only action, reachable through `ActionCli` only.
|
|
126
126
|
- MCP input must have an object-root JSON Schema. Scalar or array input is fine for HTTP, but `ActionMcp.layerHttp`, `ActionMcp.layerStdio`, and `ActionToolkit.make` throw when called with such an action. Success and error schemas may be any shape.
|
|
@@ -132,7 +132,7 @@ export const Double = Action.make("double", {
|
|
|
132
132
|
|
|
133
133
|
## Failure modes
|
|
134
134
|
|
|
135
|
-
- Throws at `make`: invalid name, name `then`, `mcp.name` longer than 128 characters or not matching the pattern.
|
|
135
|
+
- Throws at `make`: invalid name, name `then`, an `access` that is neither `"read"` nor `"write"`, `mcp.name` longer than 128 characters or not matching the pattern.
|
|
136
136
|
- Type error `Effect<..., X, ...> is not assignable` in `implement`: the handler fails with an undeclared error `X`. Add it to `errors` or handle it.
|
|
137
137
|
- `<action>: MCP input must have an object root` thrown by `ActionMcp.layerHttp`, `ActionMcp.layerStdio`, or `ActionToolkit.make`: an MCP-enabled action has non-object input. Wrap it in `Schema.Struct` or set `mcp: false`.
|
|
138
138
|
- Handler receives a string where a number was expected: the schema is `Schema.String`, not a transforming codec such as `Schema.FiniteFromString`.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -109,7 +109,7 @@ export const greeting = Effect.gen(function* () {
|
|
|
109
109
|
- `Http.layer(options, ...apps)` mounts only the supplied implementations. A group that is never passed to `layer` has no routes. A group with no HTTP-enabled actions is not built at all. The options object is required; pass `{}` for a surface with no hook.
|
|
110
110
|
- `before` runs once per request, with the selected action contract, **before the payload is decoded**, so an unauthorized caller learns nothing about the input schema and the handler never runs. It fails with the binding's own `errors`, and that failure is encoded exactly like a declared error, with the schema's `httpApiStatus`. Its services are request-time requirements, joined with the handlers'.
|
|
111
111
|
- 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`.
|
|
112
|
-
- Every response with a status of 400 or more carries `cache-control: no-store`, including hook refusals, declared errors and defects. Successful responses are left alone; only the host knows whether they are public.
|
|
112
|
+
- Every response these routes answer with a status of 400 or more carries `cache-control: no-store`, including hook refusals, declared errors and defects. Successful responses are left alone; only the host knows whether they are public. The middleware covers the action routes only, not the router's 404 for an unmatched path or anything else the host mounts.
|
|
113
113
|
- 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.
|
|
114
114
|
- 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.
|
|
115
115
|
- `Http.api` is a plain `HttpApi`. Anything Effect can do with an `HttpApi` works: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `HttpApi.addHttpApi` to combine with other APIs, `HttpApiClient.make`.
|
package/docs/ActionMcp.md
CHANGED
|
@@ -120,6 +120,7 @@ Layer.launch(layer).pipe(
|
|
|
120
120
|
|
|
121
121
|
- `protocols` is required and takes Effect's native `McpProtocol` adapters. Effect owns negotiation, revision rejection, and sessions. `[McpProtocol.v2026_07_28]` gives a stateless endpoint that needs no initialize handshake.
|
|
122
122
|
- `path` has no default. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form, whatever revision is negotiated.
|
|
123
|
+
- The native server answers any request carrying an `Origin` header with **403** unless that exact origin is listed in `allowedOrigins`, before authentication; an endpoint without the option therefore serves Origin-less non-browser clients only, and listing the origin is how a browser-hosted client is admitted.
|
|
123
124
|
- 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.
|
|
124
125
|
- Only MCP-enabled actions become tools, under `mcp.name` with the resolved hints. A group with no tools is not built.
|
|
125
126
|
- Every MCP-enabled action must have object-root input. Checked synchronously when `layerHttp` or `layerStdio` is called, before any handler is acquired; a violation throws. The `IllegalArgumentError` in the layer's error channel comes from the native transport, not from this check.
|
|
@@ -142,4 +143,5 @@ Layer.launch(layer).pipe(
|
|
|
142
143
|
- Public tool requires a token: it shares an endpoint with protected tools. Give it its own path.
|
|
143
144
|
- Client reports a broken transport from a stdio subprocess: something printed to stdout. Set `Logger.LogToStderr` and remove `console.log`.
|
|
144
145
|
- Older MCP client cannot connect: the client expects a revision not listed in `protocols`. Add the adapter for that revision.
|
|
146
|
+
- A browser-hosted client gets an empty 403 from every call: its `Origin` is not in `allowedOrigins`. Add the exact origin, or keep the endpoint for non-browser clients.
|
|
145
147
|
- 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/guarantees.md
CHANGED
|
@@ -33,7 +33,7 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
33
33
|
| Invalid JSON / content type | 400 / 415 | n/a |
|
|
34
34
|
|
|
35
35
|
- A group `schemaError` policy replaces the empty 400s on HTTP with a declared error and status. It never affects MCP. Details in [ActionGroup.md](ActionGroup.md).
|
|
36
|
-
- Every HTTP response with a status of 400 or more carries `cache-control: no-store`. Successful responses are left to the host. An error response has no other added headers: a hook refusal and a handler error are plain declared errors, and challenge headers such as `WWW-Authenticate` belong to the admission middleware that answers before the router reaches these routes.
|
|
36
|
+
- Every HTTP action response with a status of 400 or more carries `cache-control: no-store`. It is `ActionHttp`'s own router middleware, so it covers the action routes and nothing else: an `ActionMcp.layerHttp` endpoint, the router's 404 for an unmatched path, and any other route the host mounts answer without it. Successful responses are left to the host. An error response has no other added headers: a hook refusal and a handler error are plain declared errors, and challenge headers such as `WWW-Authenticate` belong to the admission middleware that answers before the router reaches these routes.
|
|
37
37
|
- Routes are `POST <apiPath>/<group>/<action>`. Operation IDs are `<group>.<action>`. Effect generates OpenAPI component names and references.
|
|
38
38
|
- `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
39
|
- HTTP strips undeclared input fields. MCP tools are strict (`Tool.Strict`): undeclared arguments are an invalid-arguments result and input schemas publish `additionalProperties: false`. HTTP is not strict, and cannot be made strict without a hack: Effect merges one `HttpApi.ParseOptions` per endpoint and uses it for payload decoding _and_ error encoding, so `onExcessProperty: "error"` also rejects a `TaggedError` instance's own `message` and `stack`, turning a declared 409 into an empty 500. Making the payload schema itself strict would require wrapping it in an open schema, which erases its OpenAPI shape. Declare the fields you accept and treat extra HTTP fields as ignored.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Define Effect action contracts once and project them onto HTTP, MCP, native Toolkits, catalogs, and CLIs.",
|
|
5
5
|
"homepage": "https://github.com/gjermundgaraba/effect-actions#readme",
|
|
6
6
|
"bugs": {
|