@gjermundgaraba/effect-actions 0.1.0-rc.0 → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +249 -75
  2. package/dist/Action.d.mts +35 -27
  3. package/dist/Action.mjs +9 -10
  4. package/dist/ActionCatalog.d.mts +31 -0
  5. package/dist/ActionCatalog.mjs +36 -0
  6. package/dist/ActionCli.d.mts +27 -0
  7. package/dist/ActionCli.mjs +26 -0
  8. package/dist/ActionCliClient.d.mts +45 -0
  9. package/dist/ActionCliClient.mjs +52 -0
  10. package/dist/ActionGroup.d.mts +25 -16
  11. package/dist/ActionGroup.mjs +18 -19
  12. package/dist/ActionHttp.d.mts +39 -0
  13. package/dist/ActionHttp.mjs +67 -0
  14. package/dist/ActionMcp.d.mts +41 -0
  15. package/dist/ActionMcp.mjs +25 -0
  16. package/dist/ActionToolkit.d.mts +37 -0
  17. package/dist/ActionToolkit.mjs +10 -0
  18. package/dist/Authentication.d.mts +46 -0
  19. package/dist/Authentication.mjs +55 -0
  20. package/dist/Testing.d.mts +36 -0
  21. package/dist/Testing.mjs +42 -0
  22. package/dist/TestingClient.d.mts +16 -0
  23. package/dist/{testing/client.mjs → TestingClient.mjs} +5 -6
  24. package/dist/{rolldown-runtime-D7D4PA-g.mjs → _virtual/_rolldown/runtime.mjs} +1 -1
  25. package/dist/internal/actions.d.mts +18 -0
  26. package/dist/internal/actions.mjs +16 -0
  27. package/dist/internal/cli.d.mts +29 -0
  28. package/dist/internal/cli.mjs +36 -0
  29. package/dist/internal/implementation.d.mts +39 -0
  30. package/dist/internal/implementation.mjs +37 -0
  31. package/dist/internal/tools.mjs +84 -0
  32. package/package.json +16 -12
  33. package/dist/authentication.d.mts +0 -27
  34. package/dist/authentication.mjs +0 -31
  35. package/dist/http.d.mts +0 -53
  36. package/dist/http.mjs +0 -74
  37. package/dist/implementation-BkLqMWtv.mjs +0 -32
  38. package/dist/implementation-D75QAcD3.d.mts +0 -21
  39. package/dist/index.d.mts +0 -6
  40. package/dist/index.mjs +0 -6
  41. package/dist/mcp.d.mts +0 -46
  42. package/dist/mcp.mjs +0 -189
  43. package/dist/testing/client.d.mts +0 -11
  44. package/dist/testing.d.mts +0 -10
  45. package/dist/testing.mjs +0 -34
@@ -0,0 +1,27 @@
1
+ import { Action_d_exports, Any, Handler } from "./Action.mjs";
2
+ import { Actions } from "./internal/actions.mjs";
3
+ import { HandlerContext, Handlers, HandlersContext, Implementation } from "./internal/implementation.mjs";
4
+ import { Options } from "./internal/cli.mjs";
5
+ import { Scope } from "effect";
6
+ import { Command } from "effect/unstable/cli";
7
+ //#region src/ActionCli.d.ts
8
+ /** Configuration for an aggregate group command. */
9
+ export interface GroupOptions {
10
+ /** Override the group command name. */
11
+ readonly name?: string;
12
+ }
13
+ type Selected<G extends Actions, Name extends G["actions"][number]["name"]> = Extract<G["actions"][number], {
14
+ readonly name: Name;
15
+ }>;
16
+ type BoundHandler<Name extends string, A extends Any, R> = Readonly<Record<Name, Handler<A, R>>>;
17
+ /**
18
+ * Project one explicitly selected local action into a native Effect CLI command.
19
+ * The action name resolves against the bound group; no HTTP fallback exists.
20
+ */
21
+ 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, Parameters extends Command.Command.Config = never>(app: Implementation<G, H, EX, RX>, name: Name, options?: Options<Selected<G, Name>["success"]["Type"], Parameters>) => Command.Command<string, never, {}, EX | import("effect/Schema").SchemaError | Extract<G["actions"][number], {
22
+ readonly name: Name;
23
+ }>["errors"][number]["Type"], Exclude<RX, Scope.Scope> | Exclude<HandlerContext<H, Name>, Scope.Scope>>;
24
+ /** Project every local action below its group namespace with default CLI options. */
25
+ export declare const group: <G extends Actions, H extends Handlers<HandlersContext<H>>, EX, RX>(app: Implementation<G, H, EX, RX>, options?: GroupOptions) => Command.Command<string, {}, {}, EX | import("effect/Schema").SchemaError | G["actions"][number]["errors"][number]["Type"], Exclude<Exclude<RX, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<HandlersContext<H>, Scope.Scope>, Command.CommandContext<string>>>;
26
+ //#endregion
27
+ export type { Options };
@@ -0,0 +1,26 @@
1
+ import { dispatch } from "./internal/implementation.mjs";
2
+ import { command as command$1 } from "./internal/cli.mjs";
3
+ import { Effect } from "effect";
4
+ import { Command } from "effect/unstable/cli";
5
+ //#region src/ActionCli.ts
6
+ const select = (group, name) => {
7
+ const action = group.actions.find((candidate) => candidate.name === name);
8
+ if (action === void 0) throw new Error(`Unknown action "${group.name}.${name}"`);
9
+ return action;
10
+ };
11
+ const local = (app, action, input) => Effect.scoped(Effect.flatMap(app.build, (handlers) => dispatch(app.group, action, handlers)(input)));
12
+ /**
13
+ * Project one explicitly selected local action into a native Effect CLI command.
14
+ * The action name resolves against the bound group; no HTTP fallback exists.
15
+ */
16
+ const command = (app, name, options) => {
17
+ const action = select(app.group, name);
18
+ return command$1(action, (input) => local(app, action, input), options);
19
+ };
20
+ /** Project every local action below its group namespace with default CLI options. */
21
+ const group = (app, options) => {
22
+ const commands = app.group.actions.map((action) => command$1(action, (input) => Effect.scoped(Effect.flatMap(app.build, (handlers) => dispatch(app.group, action, handlers)(input)))));
23
+ return Command.make(options?.name ?? app.group.name).pipe(Command.withSubcommands(commands));
24
+ };
25
+ //#endregion
26
+ export { command, group };
@@ -0,0 +1,45 @@
1
+ import { Actions } from "./internal/actions.mjs";
2
+ import { Options as Options$1 } from "./internal/cli.mjs";
3
+ import { ActionHttp_d_exports, Api, Http } from "./ActionHttp.mjs";
4
+ import { Effect } from "effect";
5
+ import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
6
+ import { Command } from "effect/unstable/cli";
7
+ import { HttpClient } from "effect/unstable/http";
8
+ //#region src/ActionCliClient.d.ts
9
+ /** Parsing, rendering and native HTTP-endpoint configuration for one remote action. */
10
+ export type Options<Output, ParsedParameters extends Command.Command.Config = never> = Options$1<Output, ParsedParameters> & {
11
+ /** Passed directly to Effect's native `HttpApiClient.endpoint`. */
12
+ readonly connection?: NonNullable<globalThis.Parameters<typeof HttpApiClient.make>[1]>;
13
+ };
14
+ /** Configuration for a remote aggregate group command. */
15
+ export interface GroupOptions {
16
+ /** Override the group command name. */
17
+ readonly name?: string;
18
+ /** Shared native HTTP-endpoint configuration for every remote action. */
19
+ readonly connection?: NonNullable<globalThis.Parameters<typeof HttpApiClient.make>[1]>;
20
+ }
21
+ type Group<Groups extends ReadonlyArray<Actions>, Name extends Groups[number]["name"]> = Extract<Groups[number], {
22
+ readonly name: Name;
23
+ }>;
24
+ type Selected<G extends Actions, Name extends G["actions"][number]["name"]> = Extract<G["actions"][number], {
25
+ readonly name: Name;
26
+ }>;
27
+ type HttpNames<G extends Actions> = Extract<G["actions"][number], {
28
+ readonly http: true;
29
+ }>["name"];
30
+ type ApiGroups<Groups extends ReadonlyArray<Actions>> = Api<Groups[number]> extends HttpApi.HttpApi<"actions", infer ApiGroups> ? ApiGroups : never;
31
+ type Endpoint<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = Extract<HttpApiGroup.EndpointsWithIdentifier<ApiGroups<Groups>, Extract<GroupName, HttpApiGroup.Identifier<ApiGroups<Groups>>>>, {
32
+ readonly identifier: ActionName;
33
+ }>;
34
+ type NativeMethod<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = HttpApiClient.Client.Method<Extract<Endpoint<Groups, GroupName, ActionName>, HttpApiEndpoint.ConstraintRequest>, never, never>;
35
+ type RemoteCommand<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = Command.Command<string, never, {}, Effect.Error<ReturnType<NativeMethod<Groups, GroupName, ActionName>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, GroupName, ActionName>>>>;
36
+ type RemoteGroupCommand<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"]> = Command.Command<string, {}, {}, Effect.Error<ReturnType<NativeMethod<Groups, GroupName, HttpNames<Group<Groups, GroupName>>>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, GroupName, HttpNames<Group<Groups, GroupName>>>>>>;
37
+ /**
38
+ * Project one HTTP action retained by the binding into a native Effect CLI command.
39
+ * Group and action selectors resolve exclusively from `http.groups`; runtime guards
40
+ * keep dynamically supplied selector strings from reaching the native client.
41
+ */
42
+ export declare const command: <const Groups extends ReadonlyArray<Actions>, const GroupName extends Groups[number]["name"], const ActionName extends HttpNames<Group<Groups, GroupName>>, ParsedParameters extends Command.Command.Config = never>(http: Http<Groups>, groupName: GroupName, actionName: ActionName, options?: Options<Selected<Group<Groups, GroupName>, ActionName>["success"]["Type"], ParsedParameters>) => RemoteCommand<Groups, GroupName, ActionName>;
43
+ /** Project all HTTP-enabled actions retained by one group below its group namespace. */
44
+ export declare const group: <const Groups extends ReadonlyArray<Actions>, const GroupName extends Groups[number]["name"]>(http: Http<Groups>, groupName: GroupName, options?: GroupOptions) => RemoteGroupCommand<Groups, GroupName>;
45
+ //#endregion
@@ -0,0 +1,52 @@
1
+ import { command as command$1 } from "./internal/cli.mjs";
2
+ import { Effect } from "effect";
3
+ import { HttpApiClient } from "effect/unstable/httpapi";
4
+ import { Command } from "effect/unstable/cli";
5
+ import { HttpClient } from "effect/unstable/http";
6
+ //#region src/ActionCliClient.ts
7
+ /**
8
+ * The native endpoint builder needs string selectors for a runtime-selected route.
9
+ * `http.groups` is checked before this widening, and `NativeMethod` restores the
10
+ * corresponding precise endpoint type at that one dynamic client boundary.
11
+ */
12
+ const erasedApi = (api) => {
13
+ return api;
14
+ };
15
+ const selectGroup = (groups, name) => {
16
+ const group = groups.find((candidate) => candidate.name === name);
17
+ if (group === void 0) throw new Error(`Unknown HTTP group "${name}"`);
18
+ return group;
19
+ };
20
+ const selectAction = (group, name) => {
21
+ const action = group.actions.find((candidate) => candidate.name === name);
22
+ if (action === void 0 || !action.http) throw new Error(`Unknown HTTP action "${group.name}.${name}"`);
23
+ return action;
24
+ };
25
+ /**
26
+ * Project one HTTP action retained by the binding into a native Effect CLI command.
27
+ * Group and action selectors resolve exclusively from `http.groups`; runtime guards
28
+ * keep dynamically supplied selector strings from reaching the native client.
29
+ */
30
+ const command = (http, groupName, actionName, options) => {
31
+ const group = selectGroup(http.groups, groupName);
32
+ const action = selectAction(group, actionName);
33
+ return command$1(action, (input) => Effect.flatMap(HttpClient.HttpClient, (httpClient) => {
34
+ const endpoint = HttpApiClient.endpoint(erasedApi(http.api), {
35
+ group: groupName,
36
+ endpoint: actionName,
37
+ httpClient,
38
+ ...options?.connection
39
+ });
40
+ return Effect.flatMap(endpoint, (method) => {
41
+ return method({ payload: input });
42
+ });
43
+ }), options);
44
+ };
45
+ /** Project all HTTP-enabled actions retained by one group below its group namespace. */
46
+ const group = (http, groupName, options) => {
47
+ const actions = selectGroup(http.groups, groupName);
48
+ const commands = actions.actions.flatMap((action) => action.http ? [options?.connection === void 0 ? command(http, groupName, action.name) : command(http, groupName, action.name, { connection: options.connection })] : []);
49
+ return Command.make(options?.name ?? actions.name).pipe(Command.withSubcommands(commands));
50
+ };
51
+ //#endregion
52
+ export { command, group };
@@ -1,21 +1,30 @@
1
- import { Any, Handler, t as Action_d_exports } from "./Action.mjs";
2
- import { t as Implementation } from "./implementation-D75QAcD3.mjs";
1
+ import { Action, Action_d_exports, Any as Any$1, Codec, Handler } from "./Action.mjs";
2
+ import { Actions, SchemaErrorPolicy } from "./internal/actions.mjs";
3
+ import { Implementation } from "./internal/implementation.mjs";
3
4
  import { Effect, Scope } from "effect";
4
5
  declare namespace ActionGroup_d_exports {
5
- export { Group, Implementation, make };
6
+ export { Any, Group, Implementation, Options, SchemaErrorPolicy, make };
6
7
  }
7
- type HandlersFrom<Actions extends ReadonlyArray<Any>> = { readonly [A in Actions[number] as A["name"]]: Handler<A, any>; };
8
- type HandlersContext<H> = { [K in keyof H]: H[K] extends ((input: never) => Effect.Effect<infer _A, infer _E, infer R>) ? R : never; }[keyof H];
9
- export interface Group<Actions extends ReadonlyArray<Any>> {
10
- readonly actions: Actions;
11
- /**
12
- * Bind every handler at once. Pass an Effect to resolve build-time services
13
- * once (`const users = yield* Users`); services yielded inside a handler are
14
- * request-scoped instead. Scoped acquisition lasts for the adapter runtime.
15
- */
16
- readonly implement: <H extends HandlersFrom<Actions>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Actions, HandlersContext<H>, EX, Exclude<RX, Scope.Scope>>;
8
+ type HandlersFrom<Actions extends ReadonlyArray<Any$1>> = { readonly [A in Actions[number] as A["name"]]: Handler<A, any>; };
9
+ /** Group-level errors join each action's own, so a shared set is declared once. */
10
+ type WithErrors<Actions extends ReadonlyArray<Any$1>, Errors extends ReadonlyArray<Codec>> = { readonly [K in keyof Actions]: Actions[K] extends Action<infer Name, infer Input, infer Output, infer Own, infer Http, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Http, Mcp> : never; };
11
+ /** What `make` needs to define a group. */
12
+ export interface Options<Name extends string, Errors extends ReadonlyArray<Codec>, PolicyErrors extends ReadonlyArray<Codec>> {
13
+ /** The `HttpApiGroup` identifier, and so the OpenAPI tag and operation-ID prefix. */
14
+ readonly name: Name;
15
+ /** Failures every action of the group may have, added to each action's own. */
16
+ readonly errors?: Errors;
17
+ /** How HTTP answers failed decoding or encoding of these actions; MCP keeps its native answers. */
18
+ readonly schemaError?: SchemaErrorPolicy<PolicyErrors>;
17
19
  }
18
- /** Duplicate action and MCP names fail at definition time. */
19
- export declare const make: <const Actions extends ReadonlyArray<Any>>(...actions: Actions) => Group<Actions>;
20
+ /** A named set of action contracts: what adapters serve and what `implement` binds. */
21
+ export interface Group<Name extends string, Actions$1 extends ReadonlyArray<Any$1>, PolicyErrors extends ReadonlyArray<Codec> = ReadonlyArray<Codec>> extends Actions<Name, Actions$1, PolicyErrors> {
22
+ /** Bind every handler at once, resolving build-time services once per adapter layer. */
23
+ readonly implement: <H extends HandlersFrom<Actions$1>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Group<Name, Actions$1, PolicyErrors>, H, NoInfer<EX>, NoInfer<Exclude<RX, Scope.Scope>>>;
24
+ }
25
+ /** Any group, with its actions erased. */
26
+ export type Any = Group<string, ReadonlyArray<Any$1>>;
27
+ /** Define a group. Invalid or duplicate action and MCP names fail here, at definition time. */
28
+ export declare function make<const Name extends string, const Actions extends ReadonlyArray<Any$1>, const Errors extends ReadonlyArray<Codec> = [], const PolicyErrors extends ReadonlyArray<Codec> = []>(options: Options<Name, Errors, PolicyErrors>, ...actions: Actions): Group<Name, WithErrors<Actions, Errors>, PolicyErrors>;
20
29
  //#endregion
21
- export { type Implementation, ActionGroup_d_exports as t };
30
+ export { ActionGroup_d_exports, type Implementation, type SchemaErrorPolicy };
@@ -1,27 +1,26 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { t as Implementation } from "./implementation-BkLqMWtv.mjs";
1
+ import { assertDistinct, assertName } from "./internal/actions.mjs";
2
+ import { Implementation } from "./internal/implementation.mjs";
3
3
  import { Effect } from "effect";
4
4
  //#region src/ActionGroup.ts
5
- var ActionGroup_exports = /* @__PURE__ */ __exportAll({ make: () => make });
6
- /** Duplicate action and MCP names fail at definition time. */
7
- const make = (...actions) => {
8
- const names = /* @__PURE__ */ new Set();
9
- const toolNames = /* @__PURE__ */ new Set();
10
- for (const action of actions) {
11
- if (names.has(action.name)) throw new Error(`Duplicate action: ${action.name}`);
12
- names.add(action.name);
13
- if (action.mcp !== false) {
14
- if (toolNames.has(action.mcp.name)) throw new Error(`Duplicate MCP tool: ${action.mcp.name}`);
15
- toolNames.add(action.mcp.name);
16
- }
17
- }
18
- return {
5
+ function make(options, ...declared) {
6
+ const { name } = options;
7
+ assertName("action group name", name);
8
+ const actions = declared.map((action) => ({
9
+ ...action,
10
+ errors: [...action.errors, ...options.errors ?? []]
11
+ }));
12
+ assertDistinct("action", actions.map((action) => action.name));
13
+ assertDistinct("MCP tool", actions.flatMap((action) => action.mcp === false ? [] : [action.mcp.name]));
14
+ const group = {
15
+ name,
19
16
  actions,
17
+ schemaError: options.schemaError,
20
18
  implement: (build) => {
21
19
  const built = Effect.isEffect(build) ? build : Effect.succeed(build);
22
- return Implementation.make(actions, built);
20
+ return Implementation.make(group, built);
23
21
  }
24
22
  };
25
- };
23
+ return group;
24
+ }
26
25
  //#endregion
27
- export { make, ActionGroup_exports as t };
26
+ export { make };
@@ -0,0 +1,39 @@
1
+ import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
+ import { Actions, PolicyError } from "./internal/actions.mjs";
3
+ import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
4
+ import { Layer, Schema } from "effect";
5
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
6
+ import { Etag, HttpPlatform, HttpRouter } from "effect/unstable/http";
7
+ import { FileSystem } from "effect/FileSystem";
8
+ import { Path } from "effect/Path";
9
+ declare namespace ActionHttp_d_exports {
10
+ export { Api, Http, Options, make };
11
+ }
12
+ /** Contract-level configuration: servers and clients must agree on it. */
13
+ export interface Options {
14
+ readonly apiPath: `/${string}`;
15
+ }
16
+ type Endpoint<A extends Any, E extends Codec> = A extends {
17
+ readonly http: false;
18
+ } ? never : A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["errors"][number] | E>, never> : never;
19
+ type ApiGroup<G extends Actions> = G extends Actions ? [Endpoint<G["actions"][number], never>] extends [never] ? never : HttpApiGroup.HttpApiGroup<G["name"], Endpoint<G["actions"][number], PolicyError<G>>> : never;
20
+ /** The native `HttpApi` of every HTTP-enabled action of `G`, one group per action group. */
21
+ export type Api<G extends Actions> = HttpApi.HttpApi<"actions", ApiGroup<G>>;
22
+ /** A namespaced HTTP binding. */
23
+ export interface Http<Groups extends ReadonlyArray<Actions>> {
24
+ /** The exact contracts bound to this HTTP adapter, in declaration order. */
25
+ readonly groups: Groups;
26
+ readonly api: Api<Groups[number]>;
27
+ /**
28
+ * Serve any number of implementations in one layer. Build failures, build
29
+ * services and request services are unions over precisely those apps.
30
+ */
31
+ readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>>(...apps: Apps) => Layer.Layer<never, BuildError<Apps[number]>, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
32
+ }
33
+ /**
34
+ * Bind the contract-level configuration once. Every route is `POST
35
+ * <apiPath>/<group>/<action>`, so action names may repeat across groups.
36
+ */
37
+ export declare function make<const G extends ReadonlyArray<Actions>>(options: Options, ...groups: G): Http<G>;
38
+ //#endregion
39
+ export { ActionHttp_d_exports };
@@ -0,0 +1,67 @@
1
+ import { assertDistinct } from "./internal/actions.mjs";
2
+ import { dispatch } from "./internal/implementation.mjs";
3
+ import { Effect, Layer } from "effect";
4
+ import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
5
+ //#region src/ActionHttp.ts
6
+ const httpActions = (group) => group.actions.filter((action) => action.http);
7
+ const endpoint = (apiPath, group, action) => HttpApiEndpoint.post(action.name, `${apiPath}/${group.name}/${action.name}`, {
8
+ payload: action.input,
9
+ success: action.success,
10
+ error: [...action.errors, ...group.schemaError?.errors ?? []]
11
+ }).annotate(OpenApi.Description, action.description);
12
+ function erasedApi(groups, options) {
13
+ const [first, ...rest] = groups.flatMap((group) => {
14
+ const [first, ...rest] = httpActions(group).map((action) => endpoint(options.apiPath, group, action));
15
+ return first === void 0 ? [] : [HttpApiGroup.make(group.name).add(first, ...rest)];
16
+ });
17
+ const empty = HttpApi.make("actions");
18
+ return first === void 0 ? empty : empty.add(first, ...rest);
19
+ }
20
+ const schemaErrorMiddleware = (policy) => {
21
+ class SchemaErrors extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: policy.errors }) {}
22
+ return {
23
+ SchemaErrors,
24
+ layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy.map(failure)))
25
+ };
26
+ };
27
+ /**
28
+ * `HttpApiBuilder.handleAll` registers a dynamically assembled record. The
29
+ * action selected by the native router determines the input before `dispatch`
30
+ * calls its handler, so this is the one adapter boundary where exact handlers
31
+ * are viewed as an erased record. `HandlersContext` remains on the dispatch
32
+ * effect and is restored by `Http.layer`'s public signature.
33
+ */
34
+ const erasedHandlers = (handlers) => {
35
+ return handlers;
36
+ };
37
+ /** Build routes for one group. Handler erasure is limited to native dynamic endpoint registration. */
38
+ const groupLayer = (options, app) => {
39
+ const actions = httpActions(app.group);
40
+ if (actions.length === 0) return Layer.empty;
41
+ const api = erasedApi([app.group], options);
42
+ const entries = (record) => Object.fromEntries(actions.map((action) => [action.name, (request) => dispatch(app.group, action, record)(request.payload)]));
43
+ const handlers = (record) => HttpApiBuilder.group(api, app.group.name, (builder) => builder.handleAll(entries(record)));
44
+ if (app.group.schemaError === void 0) return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.layer(api).pipe(Layer.provide(handlers(erasedHandlers(record))))));
45
+ const schemaErrors = schemaErrorMiddleware(app.group.schemaError);
46
+ const policyApi = api.middleware(schemaErrors.SchemaErrors);
47
+ const policyHandlers = (record) => HttpApiBuilder.group(policyApi, app.group.name, (builder) => {
48
+ const dynamicEntries = entries(record);
49
+ return builder.handleAll(dynamicEntries);
50
+ });
51
+ return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.layer(policyApi).pipe(Layer.provide(policyHandlers(erasedHandlers(record))), Layer.provide(schemaErrors.layer))));
52
+ };
53
+ function make(options, ...groups) {
54
+ assertDistinct("action group", groups.map((group) => group.name));
55
+ const layer = (...apps) => {
56
+ assertDistinct("implementation group", apps.map((app) => app.group.name));
57
+ for (const app of apps) if (!groups.includes(app.group)) throw new Error(`Implementation of group "${app.group.name}" is not served by this adapter`);
58
+ return apps.map((app) => groupLayer(options, app)).reduce((all, app) => Layer.merge(all, app), Layer.empty);
59
+ };
60
+ return {
61
+ groups,
62
+ api: erasedApi(groups, options),
63
+ layer
64
+ };
65
+ }
66
+ //#endregion
67
+ export { make };
@@ -0,0 +1,41 @@
1
+ import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
2
+ import { Cause, Layer } from "effect";
3
+ import { McpServer } from "effect/unstable/ai";
4
+ import { HttpRouter } from "effect/unstable/http";
5
+ import { Stdio } from "effect/Stdio";
6
+ //#region src/ActionMcp.d.ts
7
+ /** One Streamable HTTP MCP endpoint; its fields become the native server info. */
8
+ export interface Options {
9
+ readonly name: string;
10
+ readonly version: string;
11
+ /** Native protocol adapters to serve; negotiation and sessions are owned by Effect. */
12
+ readonly protocols: Parameters<typeof McpServer.layerHttp>[0]["protocols"];
13
+ /** Route of the Streamable HTTP endpoint; no default. */
14
+ readonly path: HttpRouter.PathInput;
15
+ /** Browser origins accepted by the native server; passed through to `McpServer.layerHttp`. */
16
+ readonly allowedOrigins?: ReadonlyArray<string>;
17
+ readonly instructions?: string;
18
+ }
19
+ /** Server information for an MCP subprocess connected through standard I/O. */
20
+ export interface StdioOptions {
21
+ readonly name: string;
22
+ readonly version: string;
23
+ /** Native protocol adapters to serve; negotiation is owned by Effect. */
24
+ readonly protocols: Parameters<typeof McpServer.layerStdio>[0]["protocols"];
25
+ readonly instructions?: string;
26
+ }
27
+ /**
28
+ * Serve MCP tools over one Streamable HTTP endpoint.
29
+ *
30
+ * Middleware provided around this layer has the normal HTTP lifetime. Native
31
+ * context capture applies: never provide request-identity tags at startup.
32
+ */
33
+ export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>>(options: Options, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>>>;
34
+ /**
35
+ * Serve MCP tools through newline-delimited JSON-RPC on standard I/O.
36
+ *
37
+ * The host supplies the `Stdio` service. Arguments are tool input only and
38
+ * never establish request identity or authority.
39
+ */
40
+ export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>>(options: StdioOptions, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | Stdio | RequestContext<Apps[number]>>;
41
+ //#endregion
@@ -0,0 +1,25 @@
1
+ import "./internal/implementation.mjs";
2
+ import { bindTools } from "./internal/tools.mjs";
3
+ import { Layer } from "effect";
4
+ import { McpServer } from "effect/unstable/ai";
5
+ //#region src/ActionMcp.ts
6
+ const registration = (apps) => {
7
+ const binding = bindTools(apps, "mcp");
8
+ return Layer.effectDiscard(McpServer.registerToolkit(binding.toolkit)).pipe(Layer.provide(binding.layer));
9
+ };
10
+ const server = (apps, transport) => registration(apps).pipe(Layer.provide(transport), Layer.fresh);
11
+ function layerHttp(options, ...apps) {
12
+ return server(apps, McpServer.layerHttp({
13
+ name: options.name,
14
+ version: options.version,
15
+ instructions: options.instructions,
16
+ path: options.path,
17
+ protocols: options.protocols,
18
+ allowedOrigins: options.allowedOrigins
19
+ }));
20
+ }
21
+ function layerStdio(options, ...apps) {
22
+ return server(apps, McpServer.layerStdio(options));
23
+ }
24
+ //#endregion
25
+ export { layerHttp, layerStdio };
@@ -0,0 +1,37 @@
1
+ import { Action_d_exports, Any } from "./Action.mjs";
2
+ import { AnyImplementation, BuildContext, BuildError, HandlerContext, Implementation } from "./internal/implementation.mjs";
3
+ import { Layer, Schema } from "effect";
4
+ import { Tool, Toolkit } from "effect/unstable/ai";
5
+ //#region src/ActionToolkit.d.ts
6
+ /** A native tool corresponding to an MCP-enabled action. */
7
+ type NativeTool<A extends Any, R> = A extends {
8
+ readonly mcp: {
9
+ readonly name: infer Name extends string;
10
+ };
11
+ } ? Tool.Tool<Name, {
12
+ readonly parameters: A["input"];
13
+ readonly success: A["success"];
14
+ readonly failure: Schema.Union<A["errors"]>;
15
+ readonly failureMode: "return";
16
+ }, R> : never;
17
+ type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
18
+ type ToolForAction<App extends AnyImplementation, A extends Any> = A extends {
19
+ readonly mcp: object;
20
+ } ? NativeTool<A, HandlerContext<HandlersOf<App>, Extract<A["name"], keyof HandlersOf<App>>>> : never;
21
+ type ToolsFor<App extends AnyImplementation> = App extends AnyImplementation ? ToolForAction<App, App["group"]["actions"][number]> : never;
22
+ type ToolkitTools<Apps extends ReadonlyArray<AnyImplementation>> = { readonly [T in ToolsFor<Apps[number]> as T["name"]]: T; };
23
+ /** Native tools and the layer that binds their action implementations. */
24
+ export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R = never> {
25
+ readonly toolkit: Toolkit.Toolkit<Tools>;
26
+ /** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
27
+ readonly layer: Layer.Layer<Tool.HandlersFor<Tools>, E, R>;
28
+ }
29
+ /**
30
+ * Project implementations into Effect's native AI toolkit.
31
+ *
32
+ * Unlike MCP, calls return the action's native success/failure values directly.
33
+ * Build services are needed to construct `layer`; request services are needed
34
+ * when the resulting toolkit handles a call.
35
+ */
36
+ export declare function make<const Apps extends ReadonlyArray<AnyImplementation>>(...apps: Apps): Binding<ToolkitTools<Apps>, BuildError<Apps[number]>, BuildContext<Apps[number]>>;
37
+ //#endregion
@@ -0,0 +1,10 @@
1
+ import "./internal/implementation.mjs";
2
+ import { bindTools } from "./internal/tools.mjs";
3
+ import "effect";
4
+ import "effect/unstable/ai";
5
+ //#region src/ActionToolkit.ts
6
+ function make(...apps) {
7
+ return bindTools(apps, "native");
8
+ }
9
+ //#endregion
10
+ export { make };
@@ -0,0 +1,46 @@
1
+ import { Context, Effect } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
3
+ import { NonEmptyReadonlyArray } from "effect/Array";
4
+ //#region src/Authentication.d.ts
5
+ /**
6
+ * Authenticate each request and provide its identity to the downstream handler.
7
+ * `authenticate` fails with the response to send instead, so the host owns its
8
+ * status, body and challenge headers. Dependencies remain native router request
9
+ * requirements. Acquired resources live until the request scope closes, including
10
+ * while the handler is running. Every response is marked `Cache-Control: no-store`,
11
+ * including private failures serialized by enclosing middleware.
12
+ */
13
+ export declare const middleware: <I, A, R>(service: Context.Key<I, A>, authenticate: Effect.Effect<NoInfer<A>, HttpServerResponse.HttpServerResponse, R>) => HttpRouter.Middleware<{
14
+ provides: I;
15
+ handles: never;
16
+ error: never;
17
+ requires: Exclude<R, HttpRouter.Provided> | Exclude<Exclude<import("effect/Types").NoInfer<I>, I>, HttpRouter.Provided>;
18
+ layerError: never;
19
+ layerRequires: never;
20
+ }>;
21
+ /** RFC 9728 metadata to publish; the host is responsible for these being valid OAuth URLs. */
22
+ export interface ProtectedResourceOptions {
23
+ /** Exact OAuth resource identifier; its path and query select the discovery path. */
24
+ readonly resource: string;
25
+ readonly authorizationServers: NonEmptyReadonlyArray<string>;
26
+ readonly scopesSupported?: ReadonlyArray<string>;
27
+ readonly resourceName?: string;
28
+ }
29
+ /** Parameters of one `WWW-Authenticate: Bearer` challenge, RFC 6750 §3. */
30
+ export interface BearerChallengeOptions {
31
+ readonly error?: "invalid_token" | "insufficient_scope";
32
+ readonly errorDescription?: string;
33
+ /** Space-separated scopes needed for this request, not all supported scopes. */
34
+ readonly scope?: string;
35
+ }
36
+ /**
37
+ * Publish RFC 9728 discovery independently from the authenticated routes.
38
+ * This supplies metadata and challenges; the host still verifies access tokens.
39
+ */
40
+ export declare const protectedResource: (options: ProtectedResourceOptions) => {
41
+ layer: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter>;
42
+ metadataUrl: string;
43
+ /** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
44
+ challenge: (challenge?: BearerChallengeOptions) => string;
45
+ };
46
+ //#endregion
@@ -0,0 +1,55 @@
1
+ import { Effect } from "effect";
2
+ import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
3
+ //#region src/Authentication.ts
4
+ /**
5
+ * Authenticate each request and provide its identity to the downstream handler.
6
+ * `authenticate` fails with the response to send instead, so the host owns its
7
+ * status, body and challenge headers. Dependencies remain native router request
8
+ * requirements. Acquired resources live until the request scope closes, including
9
+ * while the handler is running. Every response is marked `Cache-Control: no-store`,
10
+ * including private failures serialized by enclosing middleware.
11
+ */
12
+ const middleware = (service, authenticate) => HttpRouter.middleware()((httpEffect) => authenticate.pipe(Effect.matchEffect({
13
+ onFailure: Effect.succeed,
14
+ onSuccess: (identity) => Effect.provideService(httpEffect, service, identity)
15
+ }), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")))));
16
+ const quoted = (value) => `"${value.replace(/["\\]/g, "\\$&")}"`;
17
+ /**
18
+ * Publish RFC 9728 discovery independently from the authenticated routes.
19
+ * This supplies metadata and challenges; the host still verifies access tokens.
20
+ */
21
+ const protectedResource = (options) => {
22
+ const resource = new URL(options.resource);
23
+ const path = `/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`;
24
+ const discoveryUrl = new URL(resource);
25
+ discoveryUrl.pathname = path;
26
+ const metadataUrl = discoveryUrl.href;
27
+ const target = metadataUrl.slice(discoveryUrl.origin.length);
28
+ const response = HttpServerResponse.jsonUnsafe({
29
+ resource: options.resource,
30
+ authorization_servers: options.authorizationServers,
31
+ bearer_methods_supported: ["header"],
32
+ scopes_supported: options.scopesSupported,
33
+ resource_name: options.resourceName
34
+ });
35
+ return {
36
+ layer: HttpRouter.middleware((next) => Effect.gen(function* () {
37
+ const request = yield* HttpServerRequest.HttpServerRequest;
38
+ const url = new URL(request.url, resource.origin);
39
+ if ((request.method === "GET" || request.method === "HEAD") && url.href.slice(url.origin.length) === target) return response;
40
+ return yield* next;
41
+ }), { global: true }),
42
+ metadataUrl,
43
+ /** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
44
+ challenge: (challenge = {}) => {
45
+ return `Bearer ${[
46
+ ["resource_metadata", metadataUrl],
47
+ ["error", challenge.error],
48
+ ["error_description", challenge.errorDescription],
49
+ ["scope", challenge.scope]
50
+ ].flatMap(([name, value]) => value === void 0 ? [] : [`${name}=${quoted(value)}`]).join(", ")}`;
51
+ }
52
+ };
53
+ };
54
+ //#endregion
55
+ export { middleware, protectedResource };
@@ -0,0 +1,36 @@
1
+ import { Effect } from "effect";
2
+ import { HttpApi, HttpApiClient, HttpApiGroup } from "effect/unstable/httpapi";
3
+ //#region src/Testing.d.ts
4
+ /** A web handler, such as `HttpRouter.toWebHandler(routes).handler`. */
5
+ export type Handler = (request: Request) => Promise<Response>;
6
+ /**
7
+ * The native grouped HTTP client, calling `handler` in memory instead of the network.
8
+ * `baseUrl` defaults to `http://localhost`.
9
+ */
10
+ export declare const httpClient: <Id extends string, Groups extends HttpApiGroup.Constraint>(api: HttpApi.HttpApi<Id, Groups>, handler: Handler, options?: NonNullable<Parameters<typeof HttpApiClient.make>[1]>) => Effect.Effect<import("effect/Types").Simplify<{ readonly [Group in Extract<Groups, {
11
+ readonly topLevel: false;
12
+ }> as HttpApiGroup.Identifier<Group>]: HttpApiClient.Client.GroupByEndpoint<Group, never, never>; } & HttpApiClient.Client.TopLevelMethods<Groups, never, never>>, never, Exclude<import("effect/unstable/httpapi/HttpApiMiddleware").MiddlewareClient<import("effect/unstable/httpapi/HttpApiEndpoint").Middleware<HttpApiGroup.Endpoints<Groups>>>, import("effect/unstable/http/HttpClient").HttpClient>>;
13
+ /** A stateless 2026-07-28 request. */
14
+ export interface McpRequestOptions {
15
+ readonly url: string | URL;
16
+ readonly method: string;
17
+ readonly params?: McpRequestParams;
18
+ readonly headers?: ConstructorParameters<typeof Headers>[0];
19
+ }
20
+ /**
21
+ * What `JSON.stringify` accepts, not only valid JSON: `undefined` fields are
22
+ * dropped, and tests send malformed arguments on purpose.
23
+ */
24
+ export type McpRequestValue = string | number | boolean | null | undefined | ReadonlyArray<McpRequestValue> | {
25
+ readonly [key: string]: McpRequestValue;
26
+ };
27
+ /** JSON-RPC `params`; `_meta` is merged shallowly over the defaults `mcpRequest` supplies. */
28
+ export interface McpRequestParams {
29
+ readonly _meta?: {
30
+ readonly [key: string]: McpRequestValue;
31
+ };
32
+ readonly [key: string]: McpRequestValue;
33
+ }
34
+ /** Build one stateless 2026-07-28 JSON-RPC request, with client metadata defaulted. */
35
+ export declare const mcpRequest: ({ url, method, params, headers: init }: McpRequestOptions) => Request;
36
+ //#endregion