@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.
Files changed (48) hide show
  1. package/README.md +3 -3
  2. package/dist/Action.d.mts +29 -8
  3. package/dist/Action.mjs +2 -2
  4. package/dist/ActionCatalog.d.mts +2 -3
  5. package/dist/ActionCatalog.mjs +8 -6
  6. package/dist/ActionCli.d.mts +1 -1
  7. package/dist/ActionCli.mjs +1 -1
  8. package/dist/ActionCliClient.d.mts +11 -12
  9. package/dist/ActionCliClient.mjs +7 -14
  10. package/dist/ActionGroup.d.mts +1 -1
  11. package/dist/ActionGroup.mjs +3 -9
  12. package/dist/ActionHttp.d.mts +5 -7
  13. package/dist/ActionHttp.mjs +8 -6
  14. package/dist/ActionHttpClient.d.mts +8 -20
  15. package/dist/ActionHttpClient.mjs +8 -16
  16. package/dist/ActionMcp.d.mts +8 -7
  17. package/dist/ActionMcp.mjs +73 -4
  18. package/dist/ActionToolkit.d.mts +11 -12
  19. package/dist/ActionToolkit.mjs +1 -1
  20. package/dist/Authentication.d.mts +1 -1
  21. package/dist/Authentication.mjs +1 -1
  22. package/dist/Testing.d.mts +11 -9
  23. package/dist/Testing.mjs +10 -9
  24. package/dist/internal/actions.d.mts +1 -1
  25. package/dist/internal/cli.d.mts +1 -1
  26. package/dist/internal/cli.mjs +1 -1
  27. package/dist/internal/fetchClient.d.mts +6 -0
  28. package/dist/internal/fetchClient.mjs +8 -0
  29. package/dist/internal/implementation.d.mts +14 -15
  30. package/dist/internal/implementation.mjs +3 -3
  31. package/dist/internal/tools.d.mts +2 -2
  32. package/dist/internal/tools.mjs +16 -14
  33. package/docs/Action.md +27 -14
  34. package/docs/ActionCatalog.md +10 -10
  35. package/docs/ActionCli.md +8 -8
  36. package/docs/ActionCliClient.md +9 -9
  37. package/docs/ActionGroup.md +10 -8
  38. package/docs/ActionHttp.md +13 -13
  39. package/docs/ActionHttpClient.md +26 -19
  40. package/docs/ActionMcp.md +9 -6
  41. package/docs/ActionToolkit.md +5 -5
  42. package/docs/Authentication.md +2 -3
  43. package/docs/CONTEXT.md +6 -6
  44. package/docs/README.md +4 -4
  45. package/docs/Testing.md +8 -5
  46. package/docs/guarantees.md +5 -4
  47. package/docs/setup.md +6 -5
  48. package/package.json +11 -6
package/README.md CHANGED
@@ -73,10 +73,10 @@ Code that does not run Effects, such as a browser page, gets the same calls as P
73
73
  ## Install
74
74
 
75
75
  ```sh
76
- pnpm add @gjermundgaraba/effect-actions effect@4.0.0-rc.117
76
+ pnpm add @gjermundgaraba/effect-actions effect@4.0.0-rc.118
77
77
  ```
78
78
 
79
- Add `@effect/platform-node@4.0.0-rc.117` to serve from Node. Every module is a subpath import
79
+ Add `@effect/platform-node@4.0.0-rc.118` to serve from Node. Every module is a subpath import
80
80
  (`.../Action`, `.../ActionHttp`, ...); there is no package root, so a contracts-only bundle
81
81
  never loads a server.
82
82
 
@@ -101,7 +101,7 @@ in `node_modules/@gjermundgaraba/effect-actions/docs`) gives the same content.
101
101
 
102
102
  ## Status
103
103
 
104
- The `effect` peer accepts any Effect 4.0 release candidate from `4.0.0-rc.116` on; the package
104
+ The `effect` peer accepts any Effect 4.0 release candidate from `4.0.0-rc.118` on; the package
105
105
  is built and tested against the release candidate in its `devDependencies` (see [docs/setup.md](docs/setup.md)). Actions are unary JSON over HTTP and MCP: no streaming, uploads, prompts, or resources.
106
106
  Authentication and authorization belong to the application. See [docs/setup.md](docs/setup.md).
107
107
 
package/dist/Action.d.mts CHANGED
@@ -21,16 +21,25 @@ export interface McpOptions {
21
21
  readonly idempotent?: boolean;
22
22
  /** `openWorldHint`; defaults to `true`. */
23
23
  readonly openWorld?: boolean;
24
+ /**
25
+ * A string field of the encoded success that MCP sends as a raw text block rather
26
+ * than as structured content, such as a document body. Other surfaces serve the
27
+ * whole success.
28
+ */
29
+ readonly text?: string;
24
30
  }
31
+ /** The fields `mcp.text` may name: those an encoded success has as a string. */
32
+ type TextFields<Encoded> = Encoded extends object ? { readonly [K in keyof Encoded]-?: Required<Encoded>[K] extends string ? K : never; }[keyof Encoded] & string : never;
25
33
  type ResolvedMcp<Name extends string, Mcp extends false | McpOptions | undefined> = Mcp extends false ? false : {
26
34
  readonly name: Mcp extends McpOptions ? "name" extends keyof Mcp ? Extract<Mcp["name"], string> extends never ? Name : Extract<Mcp["name"], string> | (undefined extends Mcp["name"] ? Name : never) : Name : Name;
27
35
  readonly readOnly: boolean;
28
36
  readonly destructive: boolean;
29
37
  readonly idempotent: boolean;
30
38
  readonly openWorld: boolean;
39
+ readonly text?: string;
31
40
  };
32
41
  /** What `make` needs to define an action. */
33
- export interface Options<Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Acc extends Access = Access, Http extends boolean = boolean, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
42
+ export interface Options<Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Acc extends Access = Access, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
34
43
  readonly description: string;
35
44
  /** Omit for an action without arguments. */
36
45
  readonly input?: Input;
@@ -43,32 +52,44 @@ export interface Options<Input extends Codec, Output extends Codec, Errors exten
43
52
  * library itself authorizes nothing.
44
53
  */
45
54
  readonly access: Acc;
46
- /** `false` hides the action from HTTP routes and clients. */
47
- readonly http?: Http;
48
55
  /** `false` hides the action from MCP; otherwise tool metadata. */
49
56
  readonly mcp?: Mcp;
50
57
  }
51
58
  /** A pure contract: schemas and transport metadata. Implementations are bound by `ActionGroup.implement`. */
52
- export interface Action<Name extends string, Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Acc extends Access = Access, Http extends boolean = boolean, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
59
+ export interface Action<Name extends string, Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Acc extends Access = Access, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
53
60
  readonly name: Name;
54
61
  readonly description: string;
55
62
  readonly input: Input;
56
63
  readonly success: Output;
57
64
  readonly errors: Errors;
58
65
  readonly access: Acc;
59
- readonly http: Http;
60
66
  readonly mcp: ResolvedMcp<Name, Mcp>;
61
67
  }
62
68
  /** Any action, with its schemas erased. */
63
- export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, boolean, false> | Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, boolean, McpOptions> | Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, boolean, undefined>;
69
+ export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, false> | Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, McpOptions> | Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, undefined>;
64
70
  /** Receives decoded input; may fail only with the declared errors. */
65
71
  export type Handler<A extends Any, R = never> = (input: A["input"]["Type"]) => Effect.Effect<A["success"]["Type"], A["errors"][number]["Type"], R>;
66
72
  /** An empty object schema that also produces the object root MCP requires. */
67
73
  declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
74
+ type AnyOptions = Options<Codec, Codec, ReadonlyArray<Codec>, Access, false | McpOptions | undefined>;
75
+ /**
76
+ * The `mcp` option `make` received. Only a required literal `false` hides the action:
77
+ * options that may carry something else, such as a conditional spread, `false | undefined`
78
+ * or a broadly typed variable, may serve it, so its requirements are kept.
79
+ */
80
+ type McpOf<O extends AnyOptions> = [O] extends [{
81
+ readonly mcp: false;
82
+ }] ? false : O extends {
83
+ readonly mcp: infer Mcp extends false | McpOptions | undefined;
84
+ } ? Mcp : "mcp" extends keyof O ? O["mcp"] | undefined : undefined;
68
85
  /**
69
86
  * Define an action contract. Names are `[A-Za-z0-9_-]+`, other than `then`.
70
- * Actions can be local-only by setting both transports to `false`.
87
+ * HTTP serves every action of a group it binds; `mcp: false` hides one from MCP.
71
88
  */
72
- export declare function make<const Name extends string, Input extends Codec = typeof NoInput, Output extends Codec = never, const Errors extends ReadonlyArray<Codec> = [], const Acc extends Access = Access, const Http extends boolean = true, const Mcp extends false | McpOptions | undefined = undefined>(name: Name, options: Options<Input, Output, Errors, Acc, Http, Mcp>): Action<Name, Input, Output, Errors, Acc, Http, Mcp>;
89
+ export declare function make<const Name extends string, const O extends AnyOptions>(name: Name, options: O & { readonly [K in Exclude<keyof O, keyof AnyOptions>]: never; } & {
90
+ readonly mcp?: false | (McpOptions & {
91
+ readonly text?: TextFields<O["success"]["Encoded"]>;
92
+ }) | undefined;
93
+ }): Action<Name, "input" extends keyof O ? Exclude<O["input"], undefined> : typeof NoInput, O["success"], "errors" extends keyof O ? Exclude<O["errors"], undefined> : [], O["access"], McpOf<O>>;
73
94
  //#endregion
74
95
  export { Action_d_exports };
package/dist/Action.mjs CHANGED
@@ -13,7 +13,8 @@ function make(name, options) {
13
13
  readOnly,
14
14
  destructive: options.mcp?.destructive ?? !readOnly,
15
15
  idempotent: options.mcp?.idempotent ?? false,
16
- openWorld: options.mcp?.openWorld ?? true
16
+ openWorld: options.mcp?.openWorld ?? true,
17
+ ...options.mcp?.text === void 0 ? {} : { text: options.mcp.text }
17
18
  };
18
19
  if (mcp !== false && mcp.name.length > 128) throw new Error(`Invalid MCP name: ${mcp.name}`);
19
20
  if (mcp !== false) assertName("MCP name", mcp.name);
@@ -24,7 +25,6 @@ function make(name, options) {
24
25
  success: options.success,
25
26
  errors: options.errors ?? [],
26
27
  access,
27
- http: options.http !== false,
28
28
  mcp
29
29
  };
30
30
  }
@@ -8,7 +8,6 @@ export interface Entry {
8
8
  readonly group: string;
9
9
  readonly name: string;
10
10
  readonly description: string;
11
- readonly http: boolean;
12
11
  readonly mcp: Any["mcp"];
13
12
  readonly input: JsonSchema.JsonSchema;
14
13
  readonly success: JsonSchema.JsonSchema;
@@ -19,13 +18,13 @@ export interface Entry {
19
18
  }
20
19
  /** An offline contract document, not an authorization decision or a mounted endpoint. */
21
20
  export interface Catalog {
22
- readonly version: "3";
21
+ readonly version: "4";
23
22
  readonly actions: ReadonlyArray<Entry>;
24
23
  }
25
24
  /**
26
25
  * Describe the supplied groups without acquiring implementations. Schemas describe
27
26
  * encoded action values, not MCP's `{ value }` envelope or deployment-specific URLs.
28
- * Local-only actions are included; the host chooses what, if anything, to publish.
27
+ * The host chooses what, if anything, to publish.
29
28
  */
30
29
  export declare const make: (...groups: ReadonlyArray<Any$1>) => Catalog;
31
30
  //#endregion
@@ -1,9 +1,12 @@
1
1
  import { assertDistinct, policyErrors } from "./internal/actions.mjs";
2
2
  import { JsonSchema, Schema } from "effect";
3
3
  //#region src/ActionCatalog.ts
4
- /** Describe the JSON wire form; decoding on the server remains authoritative. */
4
+ /**
5
+ * Describe the JSON wire form closed, as the OpenAPI document does; decoding on the
6
+ * server remains authoritative.
7
+ */
5
8
  const describe = (codec) => {
6
- const { schema, definitions } = Schema.toJsonSchemaDocument(codec);
9
+ const { schema, definitions } = Schema.toJsonSchemaDocument(codec, { onExcessProperty: "error" });
7
10
  return {
8
11
  $schema: JsonSchema.META_SCHEMA_URI_DRAFT_2020_12,
9
12
  ...schema,
@@ -13,23 +16,22 @@ const describe = (codec) => {
13
16
  /**
14
17
  * Describe the supplied groups without acquiring implementations. Schemas describe
15
18
  * encoded action values, not MCP's `{ value }` envelope or deployment-specific URLs.
16
- * Local-only actions are included; the host chooses what, if anything, to publish.
19
+ * The host chooses what, if anything, to publish.
17
20
  */
18
21
  const make = (...groups) => {
19
22
  assertDistinct("catalog group", groups.map((group) => group.name));
20
23
  return {
21
- version: "3",
24
+ version: "4",
22
25
  actions: groups.flatMap((group) => group.actions.map((action) => ({
23
26
  id: `${group.name}.${action.name}`,
24
27
  group: group.name,
25
28
  name: action.name,
26
29
  description: action.description,
27
- http: action.http,
28
30
  mcp: action.mcp,
29
31
  input: describe(action.input),
30
32
  success: describe(action.success),
31
33
  errors: action.errors.map(describe),
32
- httpSchemaErrors: action.http && group.schemaError !== void 0 ? policyErrors(group.schemaError).map(describe) : []
34
+ httpSchemaErrors: group.schemaError === void 0 ? [] : policyErrors(group.schemaError).map(describe)
33
35
  })))
34
36
  };
35
37
  };
@@ -3,7 +3,7 @@ import { Actions } from "./internal/actions.mjs";
3
3
  import { HandlerContext, Handlers, HandlersContext, Implementation } from "./internal/implementation.mjs";
4
4
  import { Options as Options$1 } from "./internal/cli.mjs";
5
5
  import { Effect, Scope } from "effect";
6
- import { Command } from "effect/unstable/cli";
6
+ import { Command } from "effect/cli";
7
7
  //#region src/ActionCli.d.ts
8
8
  /**
9
9
  * The hook a local command runs before the selected handler. A CLI does not serialize
@@ -2,7 +2,7 @@ import { selectNamed } from "./internal/actions.mjs";
2
2
  import { dispatch } from "./internal/implementation.mjs";
3
3
  import { command as command$1 } from "./internal/cli.mjs";
4
4
  import { Effect } from "effect";
5
- import { Command } from "effect/unstable/cli";
5
+ import { Command } from "effect/cli";
6
6
  //#region src/ActionCli.ts
7
7
  /**
8
8
  * Acquire the implementation in a scope of its own, run one action through the hook
@@ -1,12 +1,11 @@
1
1
  import { Action_d_exports, Codec } from "./Action.mjs";
2
2
  import { Actions } from "./internal/actions.mjs";
3
- import { ServedNames } from "./internal/implementation.mjs";
4
3
  import { Options as Options$1 } from "./internal/cli.mjs";
5
4
  import { ActionHttp_d_exports, Api, Http } from "./ActionHttp.mjs";
6
5
  import { Effect } from "effect";
7
- import { HttpClient } from "effect/unstable/http";
8
- import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
9
- import { Command } from "effect/unstable/cli";
6
+ import { HttpClient } from "effect/http";
7
+ import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
8
+ import { Command } from "effect/cli";
10
9
  //#region src/ActionCliClient.d.ts
11
10
  /**
12
11
  * What the host configures on Effect's native client: `baseUrl`, `transformClient`,
@@ -30,20 +29,20 @@ type Group<Groups extends ReadonlyArray<Actions>, Name extends Groups[number]["n
30
29
  type Selected<G extends Actions, Name extends G["actions"][number]["name"]> = Extract<G["actions"][number], {
31
30
  readonly name: Name;
32
31
  }>;
33
- type HttpNames<G extends Actions> = ServedNames<G, "http">;
32
+ type ActionNames<G extends Actions> = G["actions"][number]["name"];
34
33
  type ApiGroups<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>> = Api<Groups[number], Errors[number]> extends HttpApi.HttpApi<"actions", infer ApiGroups> ? ApiGroups : never;
35
- type Endpoint<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = Extract<HttpApiGroup.EndpointsWithIdentifier<ApiGroups<Groups, Errors>, Extract<GroupName, HttpApiGroup.Identifier<ApiGroups<Groups, Errors>>>>, {
34
+ type Endpoint<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends ActionNames<Group<Groups, GroupName>>> = Extract<HttpApiGroup.EndpointsWithIdentifier<ApiGroups<Groups, Errors>, Extract<GroupName, HttpApiGroup.Identifier<ApiGroups<Groups, Errors>>>>, {
36
35
  readonly identifier: ActionName;
37
36
  }>;
38
- type NativeMethod<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = HttpApiClient.Client.Method<Extract<Endpoint<Groups, Errors, GroupName, ActionName>, HttpApiEndpoint.ConstraintRequest>, never, never>;
39
- type RemoteCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = Command.Command<string, never, {}, Effect.Error<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionName>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionName>>>>;
40
- type RemoteGroupCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"]> = Command.Command<string, {}, {}, Effect.Error<ReturnType<NativeMethod<Groups, Errors, GroupName, HttpNames<Group<Groups, GroupName>>>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, Errors, GroupName, HttpNames<Group<Groups, GroupName>>>>>>;
37
+ type NativeMethod<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends ActionNames<Group<Groups, GroupName>>> = HttpApiClient.Client.Method<Extract<Endpoint<Groups, Errors, GroupName, ActionName>, HttpApiEndpoint.ConstraintRequest>, never, never>;
38
+ type RemoteCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends ActionNames<Group<Groups, GroupName>>> = Command.Command<string, never, {}, Effect.Error<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionName>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionName>>>>;
39
+ type RemoteGroupCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"]> = Command.Command<string, {}, {}, Effect.Error<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionNames<Group<Groups, GroupName>>>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionNames<Group<Groups, GroupName>>>>>>;
41
40
  /**
42
- * Project one HTTP action retained by the binding into a native Effect CLI command.
41
+ * Project one action of the binding into a native Effect CLI command.
43
42
  * Group and action selectors resolve exclusively from `http.groups`; runtime guards
44
43
  * keep dynamically supplied selector strings from reaching the native client.
45
44
  */
46
- export declare const command: <const Groups extends ReadonlyArray<Actions>, const Errors extends ReadonlyArray<Codec>, const GroupName extends Groups[number]["name"], const ActionName extends HttpNames<Group<Groups, GroupName>>, ParsedParameters extends Command.Command.Config = never>(http: Http<Groups, Errors>, groupName: GroupName, actionName: ActionName, options?: Options<Selected<Group<Groups, GroupName>, ActionName>["success"]["Type"], ParsedParameters>) => RemoteCommand<Groups, Errors, GroupName, ActionName>;
47
- /** Project all HTTP-enabled actions retained by one group below its group namespace. */
45
+ export declare const command: <const Groups extends ReadonlyArray<Actions>, const Errors extends ReadonlyArray<Codec>, const GroupName extends Groups[number]["name"], const ActionName extends ActionNames<Group<Groups, GroupName>>, ParsedParameters extends Command.Command.Config = never>(http: Http<Groups, Errors>, groupName: GroupName, actionName: ActionName, options?: Options<Selected<Group<Groups, GroupName>, ActionName>["success"]["Type"], ParsedParameters>) => RemoteCommand<Groups, Errors, GroupName, ActionName>;
46
+ /** Project every action of one bound group below its group namespace. */
48
47
  export declare const group: <const Groups extends ReadonlyArray<Actions>, const Errors extends ReadonlyArray<Codec>, const GroupName extends Groups[number]["name"]>(http: Http<Groups, Errors>, groupName: GroupName, options?: GroupOptions) => RemoteGroupCommand<Groups, Errors, GroupName>;
49
48
  //#endregion
@@ -1,9 +1,9 @@
1
1
  import { selectNamed } from "./internal/actions.mjs";
2
2
  import { command as command$1 } from "./internal/cli.mjs";
3
3
  import { Effect } from "effect";
4
- import { HttpClient } from "effect/unstable/http";
5
- import { HttpApiClient } from "effect/unstable/httpapi";
6
- import { Command } from "effect/unstable/cli";
4
+ import { HttpClient } from "effect/http";
5
+ import { HttpApiClient } from "effect/http-api";
6
+ import { Command } from "effect/cli";
7
7
  //#region src/ActionCliClient.ts
8
8
  /**
9
9
  * The native endpoint builder needs string selectors for a runtime-selected route.
@@ -13,21 +13,14 @@ import { Command } from "effect/unstable/cli";
13
13
  const erasedApi = (api) => {
14
14
  return api;
15
15
  };
16
- /** An action hidden from HTTP is unknown to this binding, exactly like a missing one. */
17
- const selectAction = (group, name) => {
18
- const what = `HTTP action "${group.name}.${name}"`;
19
- const action = selectNamed(group.actions, name, what);
20
- if (!action.http) throw new Error(`Unknown ${what}`);
21
- return action;
22
- };
23
16
  /**
24
- * Project one HTTP action retained by the binding into a native Effect CLI command.
17
+ * Project one action of the binding into a native Effect CLI command.
25
18
  * Group and action selectors resolve exclusively from `http.groups`; runtime guards
26
19
  * keep dynamically supplied selector strings from reaching the native client.
27
20
  */
28
21
  const command = (http, groupName, actionName, options) => {
29
22
  const group = selectNamed(http.groups, groupName, `HTTP group "${groupName}"`);
30
- const action = selectAction(group, actionName);
23
+ const action = selectNamed(group.actions, actionName, `HTTP action "${groupName}.${actionName}"`);
31
24
  return command$1(action, (input) => Effect.flatMap(HttpClient.HttpClient, (httpClient) => {
32
25
  const endpoint = HttpApiClient.endpoint(erasedApi(http.api), {
33
26
  ...options?.connection,
@@ -40,10 +33,10 @@ const command = (http, groupName, actionName, options) => {
40
33
  });
41
34
  }), options);
42
35
  };
43
- /** Project all HTTP-enabled actions retained by one group below its group namespace. */
36
+ /** Project every action of one bound group below its group namespace. */
44
37
  const group = (http, groupName, options) => {
45
38
  const actions = selectNamed(http.groups, groupName, `HTTP group "${groupName}"`);
46
- 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 })] : []);
39
+ const commands = actions.actions.map((action) => options?.connection === void 0 ? command(http, groupName, action.name) : command(http, groupName, action.name, { connection: options.connection }));
47
40
  return Command.make(options?.name ?? actions.name).pipe(Command.withSubcommands(commands));
48
41
  };
49
42
  //#endregion
@@ -7,7 +7,7 @@ declare namespace ActionGroup_d_exports {
7
7
  }
8
8
  type HandlersFrom<Actions extends ReadonlyArray<Any$1>> = { readonly [A in Actions[number] as A["name"]]: Handler<A, any>; };
9
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 Access, infer Http, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Access, Http, Mcp> : never; };
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 Access, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Access, Mcp> : never; };
11
11
  /** What `make` needs to define a group. */
12
12
  export interface Options<Name extends string, Errors extends ReadonlyArray<Codec>, Invalid extends Codec = never, Internal extends Codec = never> {
13
13
  /** The `HttpApiGroup` identifier, and so the OpenAPI tag and operation-ID prefix. */
@@ -1,15 +1,10 @@
1
1
  import { assertDistinct, assertName } from "./internal/actions.mjs";
2
2
  import { Implementation } from "./internal/implementation.mjs";
3
- import { Effect } from "effect";
3
+ import { Effect, Predicate } from "effect";
4
4
  //#region src/ActionGroup.ts
5
- /**
6
- * The types require every handler, but a record built in plain JavaScript or through a
7
- * cast may still lack one, or hold `undefined` under its key. It fails before anything is served rather than on the
8
- * first request for the missing action.
9
- */
5
+ /** Refuse a record without an own-property function for every action, before anything is served. */
10
6
  const assertHandlers = (group, handlers) => {
11
- const bound = new Map(Object.entries(handlers));
12
- const missing = group.actions.filter((action) => bound.get(action.name) === void 0);
7
+ const missing = group.actions.filter((action) => !(Object.hasOwn(handlers, action.name) && Predicate.isFunction(handlers[action.name])));
13
8
  if (missing.length > 0) throw new Error(`Missing handlers for group "${group.name}": ${missing.map((action) => action.name).join(", ")}`);
14
9
  return handlers;
15
10
  };
@@ -21,7 +16,6 @@ function make(options, ...declared) {
21
16
  errors: [...action.errors, ...options.errors ?? []]
22
17
  }));
23
18
  assertDistinct("action", actions.map((action) => action.name));
24
- assertDistinct("MCP tool", actions.flatMap((action) => action.mcp === false ? [] : [action.mcp.name]));
25
19
  const group = {
26
20
  name,
27
21
  actions,
@@ -2,8 +2,8 @@ import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
2
  import { Actions, PolicyError } from "./internal/actions.mjs";
3
3
  import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
4
4
  import { Effect, Layer, Schema } from "effect";
5
- import { Etag, HttpPlatform, HttpRouter } from "effect/unstable/http";
6
- import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
5
+ import { Etag, HttpPlatform, HttpRouter } from "effect/http";
6
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
7
7
  import { FileSystem } from "effect/FileSystem";
8
8
  import { Path } from "effect/Path";
9
9
  declare namespace ActionHttp_d_exports {
@@ -31,11 +31,9 @@ export interface LayerOptions<Errors extends ReadonlyArray<Codec>, R> {
31
31
  */
32
32
  readonly before?: (action: Any) => Effect.Effect<void, Errors[number]["Type"], R>;
33
33
  }
34
- type Endpoint<A extends Any, E extends Codec> = A extends {
35
- readonly http: false;
36
- } ? 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;
34
+ type Endpoint<A extends Any, E extends Codec> = 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;
37
35
  type ApiGroup<G extends Actions, E extends Codec> = G extends Actions ? [Endpoint<G["actions"][number], never>] extends [never] ? never : HttpApiGroup.HttpApiGroup<G["name"], Endpoint<G["actions"][number], PolicyError<G> | E>> : never;
38
- /** The native `HttpApi` of every HTTP-enabled action of `G`, one group per action group. */
36
+ /** The native `HttpApi` of every action of `G`, one group per action group. */
39
37
  export type Api<G extends Actions, E extends Codec = never> = HttpApi.HttpApi<"actions", ApiGroup<G, E>>;
40
38
  /** A namespaced HTTP binding. */
41
39
  export interface Http<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec> = []> {
@@ -47,7 +45,7 @@ export interface Http<Groups extends ReadonlyArray<Actions>, Errors extends Read
47
45
  * around every request they answer. Build failures, build services and request
48
46
  * services are unions over precisely those apps and that hook.
49
47
  */
50
- readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>, RB = never>(apps: readonly [...Apps], options?: LayerOptions<Errors, RB>) => Layer.Layer<never, BuildError<Apps[number], "http">, BuildContext<Apps[number], "http"> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number], "http"> | RB> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
48
+ readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>, RB = never>(apps: readonly [...Apps], options?: LayerOptions<Errors, RB>) => Layer.Layer<never, BuildError<Apps[number]>, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]> | RB> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
51
49
  /**
52
50
  * Serve the OpenAPI document of `api` with `GET path`, by default
53
51
  * `<apiPath>/openapi.json`. It is a plain route: middleware provided to this layer
@@ -1,10 +1,9 @@
1
1
  import { answerSchemaError, assertDistinct, policyErrors, projectedErrors } from "./internal/actions.mjs";
2
2
  import { dispatch } from "./internal/implementation.mjs";
3
3
  import { Effect, Layer } from "effect";
4
- import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
5
- import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
4
+ import { HttpRouter, HttpServerResponse } from "effect/http";
5
+ import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/http-api";
6
6
  //#region src/ActionHttp.ts
7
- const httpActions = (group) => group.actions.filter((action) => action.http);
8
7
  const endpoint = (options, group, action) => HttpApiEndpoint.post(action.name, `${options.apiPath}/${group.name}/${action.name}`, {
9
8
  payload: action.input,
10
9
  success: action.success,
@@ -19,7 +18,7 @@ const schemaErrorMiddleware = (name, policy) => {
19
18
  };
20
19
  };
21
20
  function bind(options, group) {
22
- const [first, ...rest] = httpActions(group).map((action) => endpoint(options, group, action));
21
+ const [first, ...rest] = group.actions.map((action) => endpoint(options, group, action));
23
22
  if (first === void 0) return void 0;
24
23
  const bound = HttpApiGroup.make(group.name).add(first, ...rest);
25
24
  if (group.schemaError === void 0) return {
@@ -34,7 +33,10 @@ function bind(options, group) {
34
33
  }
35
34
  function apiOf(groups) {
36
35
  const [first, ...rest] = groups;
37
- const empty = HttpApi.make("actions").annotate(HttpApi.ParseOptions, { errors: "all" });
36
+ const empty = HttpApi.make("actions").annotate(HttpApi.PayloadParseOptions, {
37
+ errors: "all",
38
+ onExcessProperty: "error"
39
+ });
38
40
  return first === void 0 ? empty : empty.add(first, ...rest);
39
41
  }
40
42
  /**
@@ -49,7 +51,7 @@ const erasedHandlers = (handlers) => {
49
51
  };
50
52
  /** Register one group's handlers. Handler erasure is limited to native dynamic endpoint registration. */
51
53
  const groupHandlers = (api, app, middleware, before) => {
52
- const entries = (record) => Object.fromEntries(httpActions(app.group).map((action) => {
54
+ const entries = (record) => Object.fromEntries(app.group.actions.map((action) => {
53
55
  const run = dispatch(app.group, action, record, before);
54
56
  return [action.name, (request) => run(request.payload)];
55
57
  }));
@@ -1,34 +1,22 @@
1
1
  import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
2
  import { Actions } from "./internal/actions.mjs";
3
- import { ServedNames } from "./internal/implementation.mjs";
4
3
  import { ActionHttp_d_exports, Http } from "./ActionHttp.mjs";
4
+ import { ClientOptions } from "./internal/fetchClient.mjs";
5
5
  //#region src/ActionHttpClient.d.ts
6
- /** Where and how a Promise client sends its requests. */
7
- export interface Options {
8
- /**
9
- * What the binding's routes are resolved against, such as `https://api.example.com`.
10
- * Omitted, routes stay relative, which a browser resolves against the page's origin.
11
- */
12
- readonly baseUrl?: string | URL;
13
- /** Sent as `Authorization: Bearer <token>` with every call. */
14
- readonly token?: string;
15
- /**
16
- * The transport. Defaults to the global `fetch`, looked up per call. Wrap it to add
17
- * headers or to observe responses, such as a proxy's 401.
18
- */
6
+ /**
7
+ * The native `HttpApiClient.make` options except `transformResponse`, plus `fetch`;
8
+ * see the options table in docs/ActionHttpClient.md.
9
+ */
10
+ export interface Options extends Omit<ClientOptions, "transformResponse"> {
19
11
  readonly fetch?: typeof globalThis.fetch;
20
12
  }
21
- /** The HTTP-served actions of `G`: an action with a literal `http: false` has no method. */
22
- type HttpAction<G extends Actions> = Extract<G["actions"][number], {
23
- readonly name: ServedNames<G, "http">;
24
- }>;
25
13
  /**
26
14
  * One action as a Promise of its decoded success. An action whose input may be empty,
27
15
  * such as one declared without `input`, may be called without an argument.
28
16
  */
29
17
  export type Method<A extends Any> = {} extends A["input"]["Type"] ? (input?: A["input"]["Type"]) => Promise<A["success"]["Type"]> : (input: A["input"]["Type"]) => Promise<A["success"]["Type"]>;
30
- /** Every HTTP-served action of the binding, as `client.<group>.<action>(input)`. */
31
- export type Client<Groups extends ReadonlyArray<Actions>> = { readonly [G in Groups[number] as [HttpAction<G>] extends [never] ? never : G["name"]]: { readonly [A in HttpAction<G> as A["name"]]: Method<A>; }; };
18
+ /** Every action of the binding, as `client.<group>.<action>(input)`. */
19
+ export type Client<Groups extends ReadonlyArray<Actions>> = { readonly [G in Groups[number] as [G["actions"][number]] extends [never] ? never : G["name"]]: { readonly [A in G["actions"][number] as A["name"]]: Method<A>; }; };
32
20
  /**
33
21
  * A Promise client for an HTTP binding, for code that does not run Effects, such as a
34
22
  * browser page. It is Effect's native `HttpApiClient` over `fetch`, built once, and
@@ -1,24 +1,16 @@
1
+ import { fetchApiClient } from "./internal/fetchClient.mjs";
1
2
  import { Effect } from "effect";
2
- import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
3
- import { HttpApiClient } from "effect/unstable/httpapi";
4
3
  //#region src/ActionHttpClient.ts
5
- function promise(http, { baseUrl, token, fetch } = {}) {
4
+ function promise(http, { baseUrl, transformClient, fetch } = {}) {
6
5
  const send = fetch ?? ((input, init) => globalThis.fetch(input, init));
7
- const native = Effect.runSync(HttpApiClient.make(http.api, {
6
+ const native = Effect.runSync(fetchApiClient(http.api, send, {
8
7
  baseUrl,
9
- transformClient: token === void 0 ? void 0 : HttpClient.mapRequest(HttpClientRequest.bearerToken(token))
10
- }).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, send)));
11
- return Object.fromEntries(http.groups.flatMap((group) => {
12
- const endpoints = native[group.name];
13
- if (endpoints === void 0) return [];
14
- const methods = group.actions.flatMap((action) => {
15
- const endpoint = endpoints[action.name];
16
- if (endpoint === void 0) return [];
17
- const method = (input) => Effect.runPromise(endpoint({ payload: input ?? {} }));
18
- return [[action.name, method]];
19
- });
20
- return [[group.name, Object.fromEntries(methods)]];
8
+ transformClient
21
9
  }));
10
+ return Object.fromEntries(Object.entries(native).map(([group, endpoints]) => [group, Object.fromEntries(Object.entries(endpoints).map(([action, endpoint]) => {
11
+ const method = (input) => Effect.runPromise(endpoint({ payload: input ?? {} }));
12
+ return [action, method];
13
+ }))]));
22
14
  }
23
15
  //#endregion
24
16
  export { promise };
@@ -1,9 +1,9 @@
1
1
  import { Action_d_exports, Codec } from "./Action.mjs";
2
- import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
2
+ import { AnyImplementation, BuildContext, BuildError, HiddenFromMcp, RequestContext } from "./internal/implementation.mjs";
3
3
  import { SurfaceOptions } from "./internal/tools.mjs";
4
4
  import { Cause, Layer } from "effect";
5
- import { HttpRouter } from "effect/unstable/http";
6
- import { McpSchema, McpServer } from "effect/unstable/ai";
5
+ import { HttpRouter } from "effect/http";
6
+ import { McpSchema, McpServer } from "effect/ai";
7
7
  import { Stdio } from "effect/Stdio";
8
8
  //#region src/ActionMcp.d.ts
9
9
  /**
@@ -15,20 +15,21 @@ export interface Options<Errors extends ReadonlyArray<Codec> = [], R = never> ex
15
15
  /** An MCP subprocess on standard I/O: every native `McpServer.layerStdio` option except `protocols`. */
16
16
  export interface StdioOptions<Errors extends ReadonlyArray<Codec> = [], R = never> extends Omit<Parameters<typeof McpServer.layerStdio>[0], "protocols">, SurfaceOptions<Errors, R> {}
17
17
  /** The native server supplies its own request context to every tool call. */
18
- type ToolRequestContext<App, RB> = Exclude<RequestContext<App, "mcp"> | RB, McpSchema.McpRequestContext>;
18
+ type ToolRequestContext<App, RB> = Exclude<RequestContext<App, HiddenFromMcp> | RB, McpSchema.McpRequestContext>;
19
19
  /**
20
20
  * Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28 only.
21
21
  *
22
22
  * Middleware provided around this layer has the normal HTTP lifetime. Native
23
23
  * context capture applies: never provide request-identity tags at startup.
24
24
  */
25
- export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: Options<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], "mcp"> | Cause.IllegalArgumentError, BuildContext<Apps[number], "mcp"> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ToolRequestContext<Apps[number], RB>>>;
25
+ export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: Options<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], HiddenFromMcp> | Cause.IllegalArgumentError, BuildContext<Apps[number], HiddenFromMcp> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ToolRequestContext<Apps[number], RB>>>;
26
26
  /**
27
27
  * Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
28
- * 2026-07-28 only.
28
+ * 2026-07-28 only. An older host is refused deliberately, for uniformity with HTTP,
29
+ * even though stdio has no sessions.
29
30
  *
30
31
  * The host supplies the `Stdio` service. Arguments are tool input only and
31
32
  * never establish request identity or authority.
32
33
  */
33
- export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: StdioOptions<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], "mcp"> | Cause.IllegalArgumentError, BuildContext<Apps[number], "mcp"> | Stdio | ToolRequestContext<Apps[number], RB>>;
34
+ export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: StdioOptions<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], HiddenFromMcp> | Cause.IllegalArgumentError, BuildContext<Apps[number], HiddenFromMcp> | Stdio | ToolRequestContext<Apps[number], RB>>;
34
35
  //#endregion