@gjermundgaraba/effect-actions 0.1.0-rc.2 → 0.2.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.
@@ -0,0 +1,73 @@
1
+ # ActionToolkit
2
+
3
+ Implementations as Effect's native AI `Toolkit`, for programs that call tools in-process
4
+ with `LanguageModel` or by hand. No server, no MCP envelope.
5
+
6
+ ## API
7
+
8
+ ```ts
9
+ import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
10
+
11
+ function make<const Apps extends ReadonlyArray<AnyImplementation>>(
12
+ ...apps: Apps
13
+ ): Binding<ToolkitTools<Apps>, BuildError<Apps[number]>, BuildContext<Apps[number]>>;
14
+
15
+ interface Binding<Tools, E, R> {
16
+ readonly toolkit: Toolkit.Toolkit<Tools>; // native; tool names, schemas and requirements stay typed
17
+ /** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
18
+ readonly layer: Layer.Layer<Tool.HandlersFor<Tools>, E, R>;
19
+ }
20
+
21
+ // Each MCP-enabled action becomes:
22
+ Tool.Tool<
23
+ McpName,
24
+ {
25
+ parameters: A["input"];
26
+ success: A["success"];
27
+ failure: Schema.Union<A["errors"]>;
28
+ failureMode: "return";
29
+ },
30
+ HandlerRequirements
31
+ >;
32
+ ```
33
+
34
+ ## Canonical
35
+
36
+ ```ts
37
+ import { Effect, Stream } from "effect";
38
+ import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
39
+ import { UserApp } from "./handlers.js";
40
+
41
+ const binding = ActionToolkit.make(UserApp);
42
+
43
+ const program = Effect.gen(function* () {
44
+ const tools = yield* binding.toolkit; // bound handlers
45
+ const calls = yield* tools.handle("get_user", { id: "1" }); // encoded arguments
46
+ const results = yield* Stream.runCollect(calls);
47
+
48
+ return results;
49
+ }).pipe(
50
+ Effect.provideService(CurrentActor, actor), // request service, around the whole call
51
+ Effect.provide(binding.layer), // build services resolved here
52
+ );
53
+ ```
54
+
55
+ With a model: pass `binding.toolkit` as `toolkit` to `LanguageModel.generateText` and provide `binding.layer`.
56
+
57
+ ## Rules
58
+
59
+ - Selects MCP-enabled actions only, named by `mcp.name`, with the resolved hints. Groups with no selected tools are not built.
60
+ - Successes are the action's native values. There is no `{ value }` wrapper. Declared failures are returned as native tool results (`failureMode: "return"`), not raised.
61
+ - `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.
62
+ - `binding.layer` acquires each implementation once, in the layer's scope. Build-time requirements belong to the whole selected implementation.
63
+ - Each tool carries only its own handler's request requirements, not those of sibling or disabled tools.
64
+ - Supply identity at invocation, never when building the layer. Native context capture is not a security boundary.
65
+ - This is not an MCP server. Use `ActionMcp` to expose the same actions to external clients.
66
+
67
+ ## Failure modes
68
+
69
+ - Tool missing from the toolkit: the action has `mcp: false`. The Toolkit selects by MCP enablement.
70
+ - `Duplicate MCP tool: <name>` thrown at `make`: two apps expose the same tool name. Rename with `mcp.name`.
71
+ - `Service not found` for a request tag at call time: it was provided only to the stream, or only to the layer. Provide it around the whole call effect.
72
+ - Type error on `binding.layer` requirements: a build-time service is missing. Provide its Layer before `binding.layer`.
73
+ - Result is `{ value: ... }` when a plain value was expected: you are reading an MCP response, not a Toolkit result. The Toolkit never wraps.
@@ -0,0 +1,114 @@
1
+ # Authentication
2
+
3
+ Two helpers for hosts that own identity: router middleware that provides a request-scoped
4
+ identity service, and RFC 9728 protected-resource discovery with bearer challenges. Token
5
+ verification, login, and consent stay in the application.
6
+
7
+ ## API
8
+
9
+ ```ts
10
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
11
+
12
+ /** Provide `service` on every request, or send the response `authenticate` fails with. */
13
+ const middleware: <I, A, R>(
14
+ service: Context.Key<I, A>,
15
+ authenticate: Effect.Effect<A, HttpServerResponse.HttpServerResponse, R>,
16
+ ) => HttpRouter.Middleware<{
17
+ provides: I;
18
+ handles: never;
19
+ error: never;
20
+ requires: Exclude<R, HttpRouter.Provided>;
21
+ layerError: never;
22
+ layerRequires: never;
23
+ }>;
24
+
25
+ const protectedResource: (options: ProtectedResourceOptions) => {
26
+ layer: Layer.Layer<never, never, HttpRouter.HttpRouter>; // GET/HEAD /.well-known/oauth-protected-resource<path>
27
+ metadataUrl: string;
28
+ challenge: (challenge?: BearerChallengeOptions) => string; // WWW-Authenticate value
29
+ };
30
+
31
+ interface ProtectedResourceOptions {
32
+ readonly resource: string; // exact OAuth resource identifier; its path and query select the discovery path
33
+ readonly authorizationServers: NonEmptyReadonlyArray<string>;
34
+ readonly scopesSupported?: ReadonlyArray<string>;
35
+ readonly resourceName?: string;
36
+ }
37
+
38
+ interface BearerChallengeOptions {
39
+ readonly error?: "invalid_token" | "insufficient_scope";
40
+ readonly errorDescription?: string;
41
+ readonly scope?: string; // scopes needed for this request, space-separated
42
+ }
43
+ ```
44
+
45
+ ## Canonical
46
+
47
+ ```ts
48
+ import { Context, Effect, Layer, Option, Schema } from "effect";
49
+ import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
50
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
51
+
52
+ interface Actor {
53
+ readonly id: string;
54
+ readonly tenantId: string;
55
+ }
56
+
57
+ // Request-scoped tag. Never provided at startup.
58
+ export class CurrentActor extends Context.Service<CurrentActor, Actor>()("app/CurrentActor") {}
59
+
60
+ class Unauthenticated extends Schema.TaggedError<Unauthenticated>()(
61
+ "Unauthenticated",
62
+ { message: Schema.String },
63
+ { httpApiStatus: 401 },
64
+ ) {}
65
+
66
+ const discovery = Authentication.protectedResource({
67
+ resource: "https://api.example.com/mcp",
68
+ authorizationServers: ["https://auth.example.com"],
69
+ scopesSupported: ["users:read", "users:write"],
70
+ });
71
+
72
+ // The host renders the failure response: status, body, challenge.
73
+ const unauthenticated = HttpServerResponse.schemaJson(Unauthenticated)(
74
+ new Unauthenticated({ message: "A bearer token is required." }),
75
+ { status: 401, headers: { "www-authenticate": discovery.challenge({ error: "invalid_token" }) } },
76
+ ).pipe(Effect.orDie);
77
+
78
+ export const authentication = Authentication.middleware(
79
+ CurrentActor,
80
+ Effect.gen(function* () {
81
+ const request = yield* HttpServerRequest.HttpServerRequest;
82
+ const actor = yield* verifyToken(request); // Effect<Option<Actor>>
83
+
84
+ return Option.isNone(actor) ? yield* Effect.flip(unauthenticated) : actor.value;
85
+ }),
86
+ );
87
+
88
+ export const routes = Layer.mergeAll(
89
+ discovery.layer, // public
90
+ Http.layer(UserApp).pipe(Layer.provide(authentication.layer)),
91
+ mcp.pipe(Layer.provide(authentication.layer)),
92
+ );
93
+ ```
94
+
95
+ ## Rules
96
+
97
+ - `authenticate` succeeds with the identity value or fails with the `HttpServerResponse` to send. The host owns status, body, and `WWW-Authenticate`.
98
+ - Provide the middleware's `.layer` to each HTTP route layer and MCP endpoint layer that needs identity. It is native `HttpRouter.middleware`; combine with `.combine(...)`.
99
+ - Its dependencies are request requirements. Resources it acquires live until the request scope closes, including while the handler runs.
100
+ - Every response through the middleware carries `Cache-Control: no-store`, including failures serialized by enclosing middleware.
101
+ - Downstream action errors are handled by their transport. They are never serialized as authentication failures.
102
+ - 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.
103
+ - `protectedResource` publishes what it is given. The deployment must ensure `resource` and `authorizationServers` are valid OAuth URLs (HTTPS, or loopback HTTP in development).
104
+ - 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. Mount `discovery.layer` outside the authenticated layers. Caching policy is the host's.
105
+ - `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.
106
+ - Tool discovery is never filtered by actor. Authorization is a handler concern (`yield* CurrentActor`, then check).
107
+
108
+ ## Failure modes
109
+
110
+ - 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.
111
+ - Type error `HttpRouter.Request.From<"Requires", CurrentActor>` unsatisfied: the layer serving that implementation was not wrapped with `authentication.layer`.
112
+ - 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.
113
+ - Discovery requires a token: `discovery.layer` was placed under the authentication middleware. Mount it separately.
114
+ - 401 response lacks `WWW-Authenticate`: the host's failure response did not set it. Use `discovery.challenge(...)` in its headers.
@@ -0,0 +1,36 @@
1
+ # Context
2
+
3
+ Vocabulary and seams for `@gjermundgaraba/effect-actions`. The docs, the code, and the tests
4
+ use these words with these meanings.
5
+
6
+ ## Terms
7
+
8
+ - **Action**: one contract. A name, a description, an input schema, a success schema, declared error schemas, and transport metadata (`http`, `mcp`). Holds no behavior. Made by `Action.make`.
9
+ - **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`.
10
+ - **Contract**: an action or a group. Pure data; safe to import anywhere, including browsers.
11
+ - **Handler**: a function from decoded input to an Effect of decoded success, failing only with the action's declared errors, possibly requiring services.
12
+ - **Implementation**: a nominal binding of a complete handler record to a group, made by `group.implement`. Carries build-time error and requirement channels. Adapters accept implementations; `ActionCatalog` accepts contracts.
13
+ - **Builder**: the record or Effect passed to `implement`. Runs once per adapter layer that serves the implementation.
14
+ - **Build-time requirement**: a service yielded in the builder. Resolved when an adapter layer is built.
15
+ - **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.
16
+ - **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.
17
+ - **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`).
18
+ - **Declared error**: an error schema listed on an action or inherited from its group. Handlers may fail with it. Encoded on every transport with its `httpApiStatus`.
19
+ - **Schema-error policy**: a group option mapping Effect's `HttpApiSchemaError` (decode or encode failure) to a declared policy error with a status. HTTP only.
20
+ - **Policy error**: an error a schema-error policy may answer with. Part of the transport contract, never returnable by a handler.
21
+ - **Local-only action**: `http: false` and `mcp: false`. Reachable through `ActionCli` and listed in the catalog.
22
+ - **Tool**: the MCP or Toolkit projection of an MCP-enabled action, named by `mcp.name` and carrying its hints.
23
+ - **Endpoint**: one `ActionMcp.layerHttp` mount. One route, one middleware set, one tool registry.
24
+ - **Document**: the OpenAPI output of `Http.api`, produced by Effect (`OpenApi.fromApi`).
25
+ - **Catalog**: the offline JSON description of groups produced by `ActionCatalog.make`. Descriptive only.
26
+ - **Identity**: the per-request principal, provided by `Authentication.middleware` under an application-owned tag. Never provided at startup.
27
+
28
+ ## Seams
29
+
30
+ - `src/Action.ts` and `src/ActionGroup.ts` are contracts. They import nothing transport-specific and know nothing about HTTP, MCP, or the CLI.
31
+ - `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.
32
+ - 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.
33
+ - 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 and defines no application error types.
34
+ - Authentication is router middleware plus discovery metadata. Verification, login, and consent stay in the application.
35
+ - `Testing` depends only on `effect`. `TestingClient` is the single module that loads the optional MCP client peer.
36
+ - 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 ADDED
@@ -0,0 +1,85 @@
1
+ # effect-actions reference
2
+
3
+ Reference for `@gjermundgaraba/effect-actions`, written for coding agents. Every page is a
4
+ card with the same sections: **API** (signatures), **Canonical** (the one right way to write
5
+ it), **Rules** (must and never), **Failure modes** (what you see when it is wrong, and the fix).
6
+ Read the card for a module before writing code that uses it. Vocabulary is defined in
7
+ [CONTEXT.md](CONTEXT.md); the pages use those terms exactly.
8
+
9
+ Package facts that apply everywhere:
10
+
11
+ - Every module is a subpath import: `import * as Action from "@gjermundgaraba/effect-actions/Action"`. There is no package root.
12
+ - The `effect` peer is pinned to an exact release candidate (`4.0.0-rc.116`). Install the same version.
13
+ - Contracts are pure values. Nothing runs, binds, or acquires services until an adapter layer is built.
14
+
15
+ ## Pages
16
+
17
+ | Read | When you need to |
18
+ | ---------------------------------------- | -------------------------------------------------------------------------------------- |
19
+ | [setup.md](setup.md) | install, pin versions, pick entry points, know what the package does not do |
20
+ | [Action.md](Action.md) | define one contract: input, success, errors, HTTP and MCP flags, tool hints |
21
+ | [ActionGroup.md](ActionGroup.md) | group contracts, declare shared errors, set a schema-error policy, bind handlers |
22
+ | [ActionHttp.md](ActionHttp.md) | serve JSON POST routes, publish OpenAPI, call the API with `HttpApiClient` |
23
+ | [ActionMcp.md](ActionMcp.md) | serve MCP tools over Streamable HTTP or stdio |
24
+ | [ActionToolkit.md](ActionToolkit.md) | use actions as a native Effect AI `Toolkit` without a server |
25
+ | [ActionCli.md](ActionCli.md) | run handlers in-process from a terminal command |
26
+ | [ActionCliClient.md](ActionCliClient.md) | call the HTTP API from a terminal command |
27
+ | [ActionCatalog.md](ActionCatalog.md) | export contracts as an offline JSON document |
28
+ | [Authentication.md](Authentication.md) | provide a per-request identity, publish RFC 9728 discovery, build bearer challenges |
29
+ | [Testing.md](Testing.md) | call routes in memory, build MCP requests, drive the official MCP client |
30
+ | [guarantees.md](guarantees.md) | cross-cutting rules: dependency lifetimes, wire formats, spans, request context, scope |
31
+
32
+ ## Choose an adapter
33
+
34
+ - Callers speak JSON over HTTP: `ActionHttp`. Clients use Effect's `HttpApiClient` on `Http.api`.
35
+ - Callers are MCP clients: `ActionMcp.layerHttp` for a hosted endpoint, `ActionMcp.layerStdio` for a subprocess.
36
+ - Callers are an Effect AI program in the same process: `ActionToolkit`.
37
+ - Callers are humans or scripts in a terminal, handlers run locally: `ActionCli`.
38
+ - Callers are humans or scripts in a terminal, handlers run on a server: `ActionCliClient`.
39
+ - Nothing runs, only the contract is published: `ActionCatalog`.
40
+
41
+ One implementation (`group.implement(...)`) feeds every adapter.
42
+
43
+ ## Minimal program
44
+
45
+ ```ts
46
+ import { McpProtocol } from "effect/unstable/ai";
47
+ import { Effect, Layer, Schema } from "effect";
48
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
49
+ import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
50
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
51
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
52
+
53
+ const Greet = Action.make("greet", {
54
+ description: "Greet someone by name.",
55
+ input: Schema.Struct({ name: Schema.String }),
56
+ success: Schema.String,
57
+ mcp: { readOnly: true },
58
+ });
59
+
60
+ export const Actions = ActionGroup.make({ name: "greetings" }, Greet);
61
+
62
+ export const Http = ActionHttp.make({ apiPath: "/api/actions" }, Actions);
63
+
64
+ const app = Actions.implement({
65
+ greet: ({ name }) => Effect.succeed(`Hello, ${name}!`),
66
+ });
67
+
68
+ export const routes = Layer.mergeAll(
69
+ Http.layer(app),
70
+ ActionMcp.layerHttp(
71
+ { protocols: [McpProtocol.v2026_07_28], name: "greetings", version: "1.0.0", path: "/mcp" },
72
+ app,
73
+ ),
74
+ );
75
+ ```
76
+
77
+ Serve `routes` with `HttpRouter.serve` and a platform server layer. Result: `POST /api/actions/greetings/greet` and an MCP tool `greet` at `/mcp`.
78
+
79
+ ## Runnable examples
80
+
81
+ Repository directory `examples/` ([on GitHub](https://github.com/gjermundgaraba/effect-actions/tree/main/examples)):
82
+
83
+ - `quickstart.ts`, `quickstart-client.ts`: the minimal program and its typed client.
84
+ - `contracts.ts`, `handlers.ts`, `app.ts`, `server.ts`: an authenticated application with three groups, per-group middleware, two MCP endpoints, OpenAPI and Swagger.
85
+ - `toolkit.ts`, `catalog.ts`, `cli.ts`, `cli-client.ts`, `mcp-stdio.ts`: one file per other projection.
@@ -0,0 +1,91 @@
1
+ # Testing
2
+
3
+ In-memory calls against served layers. `Testing` needs no extra dependency. `TestingClient`
4
+ loads the optional `@modelcontextprotocol/client` peer and nothing else does.
5
+
6
+ ## API
7
+
8
+ ```ts
9
+ import * as Testing from "@gjermundgaraba/effect-actions/Testing";
10
+ import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
11
+
12
+ type Handler = (request: Request) => Promise<Response>; // HttpRouter.toWebHandler(routes).handler
13
+
14
+ /** Native grouped HttpApiClient calling `handler` in memory. baseUrl defaults to http://localhost. */
15
+ const httpClient: <Api extends HttpApi.HttpApi<any, any>>(
16
+ api: Api,
17
+ handler: Handler,
18
+ options?: Parameters<typeof HttpApiClient.make>[1],
19
+ ) => Effect.Effect<HttpApiClient.Client<Api>>;
20
+
21
+ /** One stateless 2026-07-28 JSON-RPC request with client metadata defaulted. */
22
+ const mcpRequest: (options: McpRequestOptions) => Request;
23
+
24
+ interface McpRequestOptions {
25
+ readonly url: string | URL;
26
+ readonly method: string; // "tools/list", "tools/call", ...
27
+ readonly params?: McpRequestParams; // anything JSON.stringify accepts, undefined fields included
28
+ readonly headers?: HeadersInit;
29
+ }
30
+
31
+ /** Connect the official client, run the callback, always close the transport. */
32
+ const withMcpClient: <A>(
33
+ options: McpClientOptions,
34
+ run: (client: Client) => Promise<A>,
35
+ ) => Promise<A>;
36
+
37
+ interface McpClientOptions {
38
+ readonly fetch: (request: Request) => Promise<Response>; // the web handler
39
+ readonly path: string;
40
+ readonly versionNegotiation?: ClientOptions["versionNegotiation"]; // official client default is legacy
41
+ readonly baseUrl?: string | URL; // http://localhost
42
+ readonly headers?: HeadersInit;
43
+ }
44
+ ```
45
+
46
+ ## Canonical
47
+
48
+ ```ts
49
+ import { Effect, Layer } from "effect";
50
+ import { HttpRouter, HttpServer } from "effect/unstable/http";
51
+ import * as Testing from "@gjermundgaraba/effect-actions/Testing";
52
+ import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
53
+ import { Http, routes } from "./quickstart.js";
54
+
55
+ const web = HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)));
56
+ // call web.dispose() when the test finishes
57
+
58
+ // HTTP, typed, in memory.
59
+ const greeting = Effect.gen(function* () {
60
+ const client = yield* Testing.httpClient(Http.api, web.handler);
61
+
62
+ return yield* client.greetings.greet({ payload: { name: "Ada" } });
63
+ });
64
+
65
+ // MCP, raw request, no handshake (2026-07-28 is stateless).
66
+ const listed = await web.handler(
67
+ Testing.mcpRequest({ url: "http://localhost/mcp", method: "tools/list" }),
68
+ );
69
+
70
+ // MCP, official client.
71
+ const result = await TestingClient.withMcpClient(
72
+ { fetch: web.handler, path: "/mcp", versionNegotiation: { mode: { pin: "2026-07-28" } } },
73
+ (client) => client.callTool({ name: "greet", arguments: { name: "Ada" } }),
74
+ );
75
+ ```
76
+
77
+ ## Rules
78
+
79
+ - `httpClient` is the same `HttpApiClient` consumers use, with its transport replaced by `handler`. Same `{ payload }` calls, same error channel.
80
+ - `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.
81
+ - `params` accepts what `JSON.stringify` accepts, so tests can send malformed arguments on purpose. `undefined` fields are dropped.
82
+ - `withMcpClient` closes the transport in a `finally` block. `versionNegotiation` is passed to the official client unchanged; for a stateless endpoint pin `2026-07-28`.
83
+ - Add `Authorization` through `headers` in both MCP helpers, or through `transformClient` in `httpClient` options.
84
+ - Direct handler tests can use `app.build` under `Effect.scoped`, but they bypass decoding, encoding, and middleware. Keep at least one adapter-level test per transport.
85
+
86
+ ## Failure modes
87
+
88
+ - `@modelcontextprotocol/client` not found: only `TestingClient` needs it. Install the peer in devDependencies, or use `Testing.mcpRequest` instead.
89
+ - Official client fails negotiation against a stateless endpoint: set `versionNegotiation: { mode: { pin: "2026-07-28" } }`.
90
+ - Handler leaks between tests: `web.dispose()` was not called. Register it with the test runner's cleanup hook.
91
+ - 404 from `httpClient`: `routes` did not include `Http.layer(app)`, or `HttpServer.layerServices` was not provided to `toWebHandler`.
@@ -0,0 +1,55 @@
1
+ # Guarantees
2
+
3
+ Rules that hold across every adapter. Module pages link here rather than repeating them.
4
+
5
+ ## Dependency lifetimes
6
+
7
+ - Handlers receive decoded input, return decoded results, and may fail only with declared errors. Anything else is a defect.
8
+ - Build-time services are those yielded in the `implement` builder Effect. They resolve once per adapter layer, in that layer's scope. An implementation served by two adapters is built twice. Share state through a Layer provided to the adapters, never through the builder.
9
+ - Request-time services are those yielded inside a handler. For HTTP-hosted adapters (`ActionHttp`, `ActionMcp.layerHttp`) they appear as `HttpRouter.Request.From<"Requires", R>` and are supplied by router middleware, `HttpRouter.provideRequest`, or request context. For `ActionToolkit`, `ActionCli`, and `ActionMcp.layerStdio` the host supplies them at invocation.
10
+ - Use distinct tags for build-time capabilities and request-scoped identity. Never provide an identity or tenant tag in a startup layer or root context. The adapters use native Effect context capture and merging: a startup value under a tag can shadow a request value or satisfy a missing one. Types verify presence, not provenance.
11
+ - Authentication must establish identity on every protected request. Types cannot verify that a handler performed authorization.
12
+ - `ActionCli` acquires the implementation per invocation and releases it after the call. `ActionToolkit.layer` acquires it once for the layer's lifetime.
13
+
14
+ ## Wire behavior
15
+
16
+ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a native `McpServer` with one `Tool` per action. The library defines no error types of its own.
17
+
18
+ | Case | HTTP | MCP |
19
+ | --------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
20
+ | 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 |
21
+ | Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: { value: <encoded> }` |
22
+ | 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` |
23
+ | Invalid output encoding | empty **400** (`HttpApiSchemaError`) | `isError: true`, text `Tool execution failed due to an internal server error.`; cause logged, not sent |
24
+ | Defect | empty **500** | same generic `isError` result; cause logged, not sent |
25
+ | Unknown path or method | 404 | n/a |
26
+ | Invalid JSON / content type | 400 / 415 | n/a |
27
+
28
+ - 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).
29
+ - Routes are `POST <apiPath>/<group>/<action>`. Operation IDs are `<group>.<action>`. Effect generates OpenAPI component names and references.
30
+ - 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 cannot be strict natively: `HttpApi.ParseOptions` also governs error encoding, where `onExcessProperty: "error"` rejects an error's own `stack`.
31
+
32
+ ## Namespaces
33
+
34
+ - Group names are unique within one `ActionHttp.make`. Action names are unique within a group. MCP tool names are unique within a group and within each Toolkit or MCP projection that serves it.
35
+ - Adapters validate only the namespace they serve. HTTP does not check tool names; MCP does not check route names.
36
+ - Each adapter builds only what it serves. A group with no HTTP actions is not built by `Http.layer`; a group with no tools is not built by MCP or Toolkit.
37
+
38
+ ## Observability
39
+
40
+ - Each handler runs in a span named `<group>.<action>`, a child of the transport's request span. Decoding and encoding happen outside the handler span.
41
+ - MCP defects and encoding failures are logged with their cause and answered generically.
42
+
43
+ ## MCP transport
44
+
45
+ - `protocols` is required. Effect owns negotiation, revision rejection, and session lifecycle; the adapter forwards the selection unchanged. `[McpProtocol.v2026_07_28]` is a stateless endpoint.
46
+ - `layerHttp` is single-endpoint Streamable HTTP, never two-endpoint HTTP+SSE, whatever revision is selected.
47
+ - Cancellation is Effect's native RPC interruption.
48
+ - Each endpoint or subprocess owns a fresh native tool registry. That isolates names, not application context.
49
+
50
+ ## Scope
51
+
52
+ - HTTP is JSON `POST` on `HttpApi`, not Effect RPC. MCP is native `McpServer`, no SDK runtime dependency.
53
+ - Actions are unary: no streaming, uploads, prompts, resources, retries, or code-execution sandbox.
54
+ - Authentication and authorization are the application's. Tool discovery is not filtered by actor. Production needs real token validation, MCP authorization discovery, request limits, and error reporting; the package supplies shapes for the first two only.
55
+ - The catalog is descriptive. Presence in it grants nothing.
package/docs/setup.md ADDED
@@ -0,0 +1,63 @@
1
+ # Setup
2
+
3
+ Install, version pins, entry points, and the boundaries of the package.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @gjermundgaraba/effect-actions effect@4.0.0-rc.116
9
+ ```
10
+
11
+ | Need | Add |
12
+ | ------------------------------ | ----------------------------------------------------- |
13
+ | Node HTTP server or stdio host | `@effect/platform-node@4.0.0-rc.116` |
14
+ | `TestingClient.withMcpClient` | `@modelcontextprotocol/client@^2.0.0` (optional peer) |
15
+
16
+ Node `^22.12.0 || ^24.0.0 || >=26.0.0`. ESM only.
17
+
18
+ ## Entry points
19
+
20
+ There is no package root. Import one module per subpath, as a namespace:
21
+
22
+ ```ts
23
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
24
+ import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
25
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
26
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
27
+ import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
28
+ import * as ActionCatalog from "@gjermundgaraba/effect-actions/ActionCatalog";
29
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
30
+ import * as ActionCliClient from "@gjermundgaraba/effect-actions/ActionCliClient";
31
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
32
+ import * as Testing from "@gjermundgaraba/effect-actions/Testing";
33
+ import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
34
+ ```
35
+
36
+ Every module imports from the `effect` package only, with two exceptions worth knowing when
37
+ bundling: `ActionMcp` and `ActionToolkit` are the only modules that import `effect/unstable/ai`,
38
+ and `TestingClient` is the only module that imports the optional `@modelcontextprotocol/client`
39
+ peer. Because there is no package root, importing a contract module never pulls in an MCP
40
+ server or the peer.
41
+
42
+ Companion Effect modules you will import alongside: `effect/unstable/httpapi` (`HttpApiClient`, `OpenApi`, `HttpApiSwagger`, `HttpApiScalar`), `effect/unstable/http` (`HttpRouter`, `HttpServerResponse`, `FetchHttpClient`), `effect/unstable/ai` (`McpProtocol`), `effect/unstable/cli` (`Command`, `Flag`, `Argument`).
43
+
44
+ ## Rules
45
+
46
+ - Pin `effect` to the exact release candidate the package declares as peer. Mixed rc versions fail at the type level in ways that look like library bugs.
47
+ - Never import from `dist/` paths or from a package root. Only the subpaths above are public.
48
+ - Schemas passed to `Action.make` must be service-free (`Schema.Codec<_, _, never, never>`). Handlers may require services.
49
+
50
+ ## Scope
51
+
52
+ What the package does, and nothing else:
53
+
54
+ - HTTP means JSON `POST` endpoints built on Effect's `HttpApi`. It is not Effect RPC.
55
+ - MCP means one native `McpServer` `Tool` per action. There is no MCP SDK runtime dependency.
56
+ - Actions are unary: one decoded input, one decoded success or one declared error. No streaming, uploads, prompts, resources, retries, or code-execution sandbox.
57
+ - Authentication and authorization are the application's. The library provides a middleware shape and discovery metadata, not a token verifier. Tool discovery is never filtered by actor.
58
+
59
+ ## Failure modes
60
+
61
+ - `Cannot find module '@gjermundgaraba/effect-actions'`: there is no root export. Import a subpath.
62
+ - Type errors inside `effect/unstable/*` after install: `effect` version drift. Align to the peer pin.
63
+ - `@modelcontextprotocol/client` resolution errors in a server build: something imported `TestingClient`. Only tests should.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba/effect-actions",
3
- "version": "0.1.0-rc.2",
3
+ "version": "0.2.0",
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": {
@@ -12,7 +12,8 @@
12
12
  "url": "git+https://github.com/gjermundgaraba/effect-actions.git"
13
13
  },
14
14
  "files": [
15
- "dist"
15
+ "dist",
16
+ "docs"
16
17
  ],
17
18
  "type": "module",
18
19
  "exports": {
@@ -30,18 +31,17 @@
30
31
  "./package.json": "./package.json"
31
32
  },
32
33
  "publishConfig": {
33
- "access": "public",
34
- "tag": "rc"
34
+ "access": "public"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@effect/platform-node": "4.0.0-rc.116",
38
38
  "@modelcontextprotocol/client": "2.0.0",
39
- "@types/node": "26.5.1",
39
+ "@types/node": "26.6.2",
40
40
  "effect": "4.0.0-rc.116",
41
- "tsx": "4.23.13",
41
+ "tsx": "4.23.15",
42
42
  "typescript": "7.0.2",
43
- "vite": "npm:@voidzero-dev/vite-plus-core@0.3.2",
44
- "vite-plus": "0.3.2"
43
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
44
+ "vite-plus": "0.3.3"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@modelcontextprotocol/client": "^2.0.0",
@@ -55,7 +55,7 @@
55
55
  "devEngines": {
56
56
  "packageManager": {
57
57
  "name": "pnpm",
58
- "version": "12.4.1",
58
+ "version": "12.5.1",
59
59
  "onFail": "download"
60
60
  }
61
61
  },
@@ -68,6 +68,7 @@
68
68
  "test": "vp test",
69
69
  "check": "vp check",
70
70
  "example": "node --import tsx examples/server.ts",
71
- "test:package": "vp run build && node scripts/test-package.mjs"
71
+ "test:package": "vp run build && node scripts/test-package.mjs",
72
+ "docs:sync": "node --import tsx scripts/sync-skill.ts"
72
73
  }
73
74
  }