@gjermundgaraba/effect-actions 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Action.d.mts +16 -8
- package/dist/Action.mjs +0 -1
- package/dist/ActionCatalog.d.mts +2 -3
- package/dist/ActionCatalog.mjs +3 -4
- package/dist/ActionCliClient.d.mts +8 -9
- package/dist/ActionCliClient.mjs +4 -11
- package/dist/ActionGroup.d.mts +1 -1
- package/dist/ActionGroup.mjs +3 -8
- package/dist/ActionHttp.d.mts +3 -5
- package/dist/ActionHttp.mjs +2 -3
- package/dist/ActionHttpClient.d.mts +8 -20
- package/dist/ActionHttpClient.mjs +8 -16
- package/dist/ActionMcp.d.mts +6 -5
- package/dist/ActionToolkit.d.mts +10 -11
- package/dist/Testing.d.mts +8 -7
- package/dist/Testing.mjs +6 -7
- package/dist/internal/fetchClient.d.mts +6 -0
- package/dist/internal/fetchClient.mjs +8 -0
- package/dist/internal/implementation.d.mts +14 -15
- package/dist/internal/implementation.mjs +3 -3
- package/docs/Action.md +11 -10
- package/docs/ActionCatalog.md +9 -9
- package/docs/ActionCli.md +7 -7
- package/docs/ActionCliClient.md +4 -4
- package/docs/ActionGroup.md +2 -2
- package/docs/ActionHttp.md +7 -7
- package/docs/ActionHttpClient.md +24 -17
- package/docs/ActionMcp.md +2 -2
- package/docs/ActionToolkit.md +1 -1
- package/docs/CONTEXT.md +2 -3
- package/docs/guarantees.md +1 -1
- package/package.json +1 -1
package/dist/Action.d.mts
CHANGED
|
@@ -30,7 +30,7 @@ type ResolvedMcp<Name extends string, Mcp extends false | McpOptions | undefined
|
|
|
30
30
|
readonly openWorld: boolean;
|
|
31
31
|
};
|
|
32
32
|
/** 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,
|
|
33
|
+
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
34
|
readonly description: string;
|
|
35
35
|
/** Omit for an action without arguments. */
|
|
36
36
|
readonly input?: Input;
|
|
@@ -43,32 +43,40 @@ export interface Options<Input extends Codec, Output extends Codec, Errors exten
|
|
|
43
43
|
* library itself authorizes nothing.
|
|
44
44
|
*/
|
|
45
45
|
readonly access: Acc;
|
|
46
|
-
/** `false` hides the action from HTTP routes and clients. */
|
|
47
|
-
readonly http?: Http;
|
|
48
46
|
/** `false` hides the action from MCP; otherwise tool metadata. */
|
|
49
47
|
readonly mcp?: Mcp;
|
|
50
48
|
}
|
|
51
49
|
/** 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,
|
|
50
|
+
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
51
|
readonly name: Name;
|
|
54
52
|
readonly description: string;
|
|
55
53
|
readonly input: Input;
|
|
56
54
|
readonly success: Output;
|
|
57
55
|
readonly errors: Errors;
|
|
58
56
|
readonly access: Acc;
|
|
59
|
-
readonly http: Http;
|
|
60
57
|
readonly mcp: ResolvedMcp<Name, Mcp>;
|
|
61
58
|
}
|
|
62
59
|
/** Any action, with its schemas erased. */
|
|
63
|
-
export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>, Access,
|
|
60
|
+
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
61
|
/** Receives decoded input; may fail only with the declared errors. */
|
|
65
62
|
export type Handler<A extends Any, R = never> = (input: A["input"]["Type"]) => Effect.Effect<A["success"]["Type"], A["errors"][number]["Type"], R>;
|
|
66
63
|
/** An empty object schema that also produces the object root MCP requires. */
|
|
67
64
|
declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
|
|
65
|
+
type AnyOptions = Options<Codec, Codec, ReadonlyArray<Codec>, Access, false | McpOptions | undefined>;
|
|
66
|
+
/**
|
|
67
|
+
* The `mcp` option `make` received. Only a required literal `false` hides the action:
|
|
68
|
+
* options that may carry something else, such as a conditional spread, `false | undefined`
|
|
69
|
+
* or a broadly typed variable, may serve it, so its requirements are kept.
|
|
70
|
+
*/
|
|
71
|
+
type McpOf<O extends AnyOptions> = [O] extends [{
|
|
72
|
+
readonly mcp: false;
|
|
73
|
+
}] ? false : O extends {
|
|
74
|
+
readonly mcp: infer Mcp extends false | McpOptions | undefined;
|
|
75
|
+
} ? Mcp : "mcp" extends keyof O ? O["mcp"] | undefined : undefined;
|
|
68
76
|
/**
|
|
69
77
|
* Define an action contract. Names are `[A-Za-z0-9_-]+`, other than `then`.
|
|
70
|
-
*
|
|
78
|
+
* HTTP serves every action of a group it binds; `mcp: false` hides one from MCP.
|
|
71
79
|
*/
|
|
72
|
-
export declare function make<const Name extends string,
|
|
80
|
+
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; }): 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
81
|
//#endregion
|
|
74
82
|
export { Action_d_exports };
|
package/dist/Action.mjs
CHANGED
package/dist/ActionCatalog.d.mts
CHANGED
|
@@ -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: "
|
|
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
|
-
*
|
|
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
|
package/dist/ActionCatalog.mjs
CHANGED
|
@@ -13,23 +13,22 @@ const describe = (codec) => {
|
|
|
13
13
|
/**
|
|
14
14
|
* Describe the supplied groups without acquiring implementations. Schemas describe
|
|
15
15
|
* encoded action values, not MCP's `{ value }` envelope or deployment-specific URLs.
|
|
16
|
-
*
|
|
16
|
+
* The host chooses what, if anything, to publish.
|
|
17
17
|
*/
|
|
18
18
|
const make = (...groups) => {
|
|
19
19
|
assertDistinct("catalog group", groups.map((group) => group.name));
|
|
20
20
|
return {
|
|
21
|
-
version: "
|
|
21
|
+
version: "4",
|
|
22
22
|
actions: groups.flatMap((group) => group.actions.map((action) => ({
|
|
23
23
|
id: `${group.name}.${action.name}`,
|
|
24
24
|
group: group.name,
|
|
25
25
|
name: action.name,
|
|
26
26
|
description: action.description,
|
|
27
|
-
http: action.http,
|
|
28
27
|
mcp: action.mcp,
|
|
29
28
|
input: describe(action.input),
|
|
30
29
|
success: describe(action.success),
|
|
31
30
|
errors: action.errors.map(describe),
|
|
32
|
-
httpSchemaErrors:
|
|
31
|
+
httpSchemaErrors: group.schemaError === void 0 ? [] : policyErrors(group.schemaError).map(describe)
|
|
33
32
|
})))
|
|
34
33
|
};
|
|
35
34
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
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";
|
|
@@ -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
|
|
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
|
|
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
|
|
39
|
-
type RemoteCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends
|
|
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,
|
|
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
|
|
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
|
|
47
|
-
/** Project
|
|
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
|
package/dist/ActionCliClient.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
package/dist/ActionGroup.d.mts
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/ActionGroup.mjs
CHANGED
|
@@ -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
|
|
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
|
};
|
package/dist/ActionHttp.d.mts
CHANGED
|
@@ -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
|
|
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]
|
|
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
|
package/dist/ActionHttp.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import { Effect, Layer } from "effect";
|
|
|
4
4
|
import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
|
|
5
5
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
|
|
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] =
|
|
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 {
|
|
@@ -49,7 +48,7 @@ const erasedHandlers = (handlers) => {
|
|
|
49
48
|
};
|
|
50
49
|
/** Register one group's handlers. Handler erasure is limited to native dynamic endpoint registration. */
|
|
51
50
|
const groupHandlers = (api, app, middleware, before) => {
|
|
52
|
-
const entries = (record) => Object.fromEntries(
|
|
51
|
+
const entries = (record) => Object.fromEntries(app.group.actions.map((action) => {
|
|
53
52
|
const run = dispatch(app.group, action, record, before);
|
|
54
53
|
return [action.name, (request) => run(request.payload)];
|
|
55
54
|
}));
|
|
@@ -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
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
31
|
-
export type Client<Groups extends ReadonlyArray<Actions>> = { readonly [G in Groups[number] as [
|
|
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,
|
|
4
|
+
function promise(http, { baseUrl, transformClient, fetch } = {}) {
|
|
6
5
|
const send = fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
7
|
-
const native = Effect.runSync(
|
|
6
|
+
const native = Effect.runSync(fetchApiClient(http.api, send, {
|
|
8
7
|
baseUrl,
|
|
9
|
-
transformClient
|
|
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 };
|
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import { HttpRouter } from "effect/unstable/http";
|
|
@@ -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,
|
|
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],
|
|
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],
|
|
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
|
package/dist/ActionToolkit.d.mts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { Action_d_exports, Any, Codec } from "./Action.mjs";
|
|
2
|
-
import { AnyImplementation, BuildContext, BuildError, HandlerContext, Implementation } from "./internal/implementation.mjs";
|
|
2
|
+
import { AnyImplementation, BuildContext, BuildError, HandlerContext, HiddenFromMcp, Implementation } from "./internal/implementation.mjs";
|
|
3
3
|
import { SurfaceOptions } from "./internal/tools.mjs";
|
|
4
4
|
import { Layer, Schema } from "effect";
|
|
5
5
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
6
6
|
//#region src/ActionToolkit.d.ts
|
|
7
7
|
/** What the in-process caller binds around the implementations it projects. */
|
|
8
8
|
export type Options<Errors extends ReadonlyArray<Codec> = [], R = never> = SurfaceOptions<Errors, R>;
|
|
9
|
-
/** A native tool corresponding to an MCP
|
|
10
|
-
type NativeTool<A extends Any, E extends Codec, R> = A extends {
|
|
11
|
-
readonly
|
|
12
|
-
readonly name: infer Name extends string;
|
|
13
|
-
};
|
|
9
|
+
/** A native tool corresponding to an action MCP may serve, named by its tool metadata. */
|
|
10
|
+
type NativeTool<A extends Any, E extends Codec, R> = Extract<A["mcp"], object> extends {
|
|
11
|
+
readonly name: infer Name extends string;
|
|
14
12
|
} ? Tool.Tool<Name, {
|
|
15
13
|
readonly parameters: A["input"];
|
|
16
14
|
readonly success: A["success"];
|
|
@@ -18,10 +16,11 @@ type NativeTool<A extends Any, E extends Codec, R> = A extends {
|
|
|
18
16
|
readonly failureMode: "return";
|
|
19
17
|
}, R> : never;
|
|
20
18
|
type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
/**
|
|
20
|
+
* A tool needs what its handler needs, plus what the binding's `before` hook needs. The
|
|
21
|
+
* rule is `ActionMcp`'s: only an action certainly hidden from MCP has no tool.
|
|
22
|
+
*/
|
|
23
|
+
type ToolForAction<App extends AnyImplementation, A extends Any, E extends Codec, RB> = A extends HiddenFromMcp ? never : NativeTool<A, E, HandlerContext<HandlersOf<App>, Extract<A["name"], keyof HandlersOf<App>>> | RB>;
|
|
25
24
|
type ToolsFor<App extends AnyImplementation, E extends Codec, RB> = App extends AnyImplementation ? ToolForAction<App, App["group"]["actions"][number], E, RB> : never;
|
|
26
25
|
type ToolkitTools<Apps extends ReadonlyArray<AnyImplementation>, E extends Codec, RB> = { readonly [T in ToolsFor<Apps[number], E, RB> as T["name"]]: T; };
|
|
27
26
|
/** Native tools and the layer that binds their action implementations. */
|
|
@@ -37,5 +36,5 @@ export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R =
|
|
|
37
36
|
* Build services are needed to construct `layer`; request services are needed
|
|
38
37
|
* when the resulting toolkit handles a call.
|
|
39
38
|
*/
|
|
40
|
-
export declare function make<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options?: Options<Errors, RB>): Binding<ToolkitTools<Apps, Errors[number], RB>, BuildError<Apps[number],
|
|
39
|
+
export declare function make<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options?: Options<Errors, RB>): Binding<ToolkitTools<Apps, Errors[number], RB>, BuildError<Apps[number], HiddenFromMcp>, BuildContext<Apps[number], HiddenFromMcp>>;
|
|
41
40
|
//#endregion
|
package/dist/Testing.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ClientOptions } from "./internal/fetchClient.mjs";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi";
|
|
3
4
|
//#region src/Testing.d.ts
|
|
4
5
|
/** A web handler, such as `HttpRouter.toWebHandler(routes).handler`. */
|
|
5
6
|
export type Handler = (request: Request) => Promise<Response>;
|
|
@@ -7,9 +8,9 @@ export type Handler = (request: Request) => Promise<Response>;
|
|
|
7
8
|
* The native grouped HTTP client, calling `handler` in memory instead of the network.
|
|
8
9
|
* `baseUrl` defaults to `http://localhost`.
|
|
9
10
|
*/
|
|
10
|
-
export declare const httpClient: <Id extends string, Groups extends HttpApiGroup.Constraint>(api: HttpApi.HttpApi<Id, Groups>, handler: Handler, options?:
|
|
11
|
+
export declare const httpClient: <Id extends string, Groups extends HttpApiGroup.Constraint>(api: HttpApi.HttpApi<Id, Groups>, handler: Handler, options?: ClientOptions) => import("effect/Effect").Effect<import("effect/Types").Simplify<{ readonly [Group in Extract<Groups, {
|
|
11
12
|
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
|
+
}> as HttpApiGroup.Identifier<Group>]: import("effect/unstable/httpapi/HttpApiClient").Client.GroupByEndpoint<Group, never, never>; } & import("effect/unstable/httpapi/HttpApiClient").Client.TopLevelMethods<Groups, never, never>>, never, Exclude<Exclude<import("effect/unstable/httpapi/HttpApiMiddleware").MiddlewareClient<import("effect/unstable/httpapi/HttpApiEndpoint").Middleware<HttpApiGroup.Endpoints<Groups>>>, import("effect/unstable/http/HttpClient").HttpClient>, never>>;
|
|
13
14
|
/** A stateless 2026-07-28 request. */
|
|
14
15
|
export interface McpRequestOptions {
|
|
15
16
|
readonly url: string | URL;
|
|
@@ -47,8 +48,8 @@ export interface McpCallOptions {
|
|
|
47
48
|
/**
|
|
48
49
|
* A tool call's outcome with the library's wire envelope removed: the success from
|
|
49
50
|
* `structuredContent.value`, or the error text of an `isError` result, parsed as JSON
|
|
50
|
-
* when it is JSON (a declared error) and kept as the text otherwise
|
|
51
|
-
* server's own message, such as for invalid arguments).
|
|
51
|
+
* when it is JSON (a declared error, whatever its shape) and kept as the text otherwise
|
|
52
|
+
* (the native server's own message, such as for invalid arguments).
|
|
52
53
|
*/
|
|
53
54
|
export type McpCallResult = {
|
|
54
55
|
readonly isError: false;
|
|
@@ -62,5 +63,5 @@ export type McpCallResult = {
|
|
|
62
63
|
* outcome. A response that is not an HTTP 200 carrying a tool result, such as an
|
|
63
64
|
* authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
|
|
64
65
|
*/
|
|
65
|
-
export declare const mcpCall: (handler: Handler, { url, name, headers,
|
|
66
|
+
export declare const mcpCall: (handler: Handler, { url, name, headers, arguments: args }: McpCallOptions) => Promise<McpCallResult>;
|
|
66
67
|
//#endregion
|
package/dist/Testing.mjs
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
1
|
+
import { fetchApiClient } from "./internal/fetchClient.mjs";
|
|
2
|
+
import { Option, Predicate, Schema } from "effect";
|
|
4
3
|
//#region src/Testing.ts
|
|
5
4
|
/**
|
|
6
5
|
* The native grouped HTTP client, calling `handler` in memory instead of the network.
|
|
7
6
|
* `baseUrl` defaults to `http://localhost`.
|
|
8
7
|
*/
|
|
9
|
-
const httpClient = (api, handler, options) =>
|
|
8
|
+
const httpClient = (api, handler, options) => fetchApiClient(api, (input, init) => handler(new Request(input, init)), {
|
|
10
9
|
baseUrl: "http://localhost",
|
|
11
10
|
...options
|
|
12
|
-
})
|
|
11
|
+
});
|
|
13
12
|
/** Build one stateless 2026-07-28 JSON-RPC request, with client metadata defaulted. */
|
|
14
13
|
const mcpRequest = ({ url, method, params = {}, headers: init }) => {
|
|
15
14
|
const headers = new Headers(init);
|
|
@@ -57,13 +56,13 @@ const parseJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json))
|
|
|
57
56
|
* outcome. A response that is not an HTTP 200 carrying a tool result, such as an
|
|
58
57
|
* authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
|
|
59
58
|
*/
|
|
60
|
-
const mcpCall = async (handler, { url, name, headers,
|
|
59
|
+
const mcpCall = async (handler, { url, name, headers, arguments: args = {} }) => {
|
|
61
60
|
const response = await handler(mcpRequest({
|
|
62
61
|
url,
|
|
63
62
|
method: "tools/call",
|
|
64
63
|
params: {
|
|
65
64
|
name,
|
|
66
|
-
arguments:
|
|
65
|
+
arguments: args
|
|
67
66
|
},
|
|
68
67
|
...headers === void 0 ? {} : { headers }
|
|
69
68
|
}));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { HttpApi, HttpApiClient, HttpApiGroup } from "effect/unstable/httpapi";
|
|
3
|
+
//#region src/internal/fetchClient.d.ts
|
|
4
|
+
/** The native `HttpApiClient.make` options: `baseUrl`, `transformClient`, `transformResponse`. */
|
|
5
|
+
export type ClientOptions = NonNullable<Parameters<typeof HttpApiClient.make>[1]>;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
3
|
+
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
4
|
+
//#region src/internal/fetchClient.ts
|
|
5
|
+
/** The native grouped `HttpApiClient` of `api`, sending every request through `fetch`. */
|
|
6
|
+
const fetchApiClient = (api, fetch, options) => HttpApiClient.make(api, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch));
|
|
7
|
+
//#endregion
|
|
8
|
+
export { fetchApiClient };
|
|
@@ -30,21 +30,20 @@ export declare class Implementation<G extends Actions, H, EX, RX> {
|
|
|
30
30
|
}
|
|
31
31
|
/** Any nominal implementation of `G`, with its record and channels erased. */
|
|
32
32
|
export type AnyImplementation<G extends Actions = Actions> = Implementation<G, any, any, any>;
|
|
33
|
-
/**
|
|
34
|
-
export type
|
|
33
|
+
/** What hides an action from MCP and Toolkit types: an `mcp` type of exactly `false`. The one rule both use. */
|
|
34
|
+
export type HiddenFromMcp = {
|
|
35
|
+
readonly mcp: false;
|
|
36
|
+
};
|
|
35
37
|
/**
|
|
36
|
-
* Names of the actions of `G`
|
|
37
|
-
*
|
|
38
|
+
* Names of the actions of `G` a surface serves: all of them, less those matching
|
|
39
|
+
* `Hidden`. An action whose `mcp` may be `false` at runtime does not match, so it keeps
|
|
40
|
+
* its requirements.
|
|
38
41
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} :
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
export type
|
|
46
|
-
/** Handler-acquisition failures of the implementations `T` acquires, or of a union of them. */
|
|
47
|
-
export type BuildError<App, T extends Transport> = App extends Implementation<infer G, any, infer EX, any> ? [ServedNames<G, T>] extends [never] ? never : EX : never;
|
|
48
|
-
/** Handler-acquisition requirements of the implementations `T` acquires, or of a union of them. */
|
|
49
|
-
export type BuildContext<App, T extends Transport> = App extends Implementation<infer G, any, any, infer RX> ? [ServedNames<G, T>] extends [never] ? never : RX : never;
|
|
42
|
+
type ServedNames<G extends Actions, Hidden> = Exclude<G["actions"][number], Hidden>["name"];
|
|
43
|
+
/** Per-request requirements of the handlers a surface can invoke, or of a union of implementations. */
|
|
44
|
+
export type RequestContext<App, Hidden = never> = App extends Implementation<infer G, infer H, any, any> ? { readonly [K in Extract<ServedNames<G, Hidden>, keyof H>]: HandlerContext<H, K>; }[Extract<ServedNames<G, Hidden>, keyof H>] : never;
|
|
45
|
+
/** Handler-acquisition failures of the implementations a surface acquires, or of a union of them. */
|
|
46
|
+
export type BuildError<App, Hidden = never> = App extends Implementation<infer G, any, infer EX, any> ? [ServedNames<G, Hidden>] extends [never] ? never : EX : never;
|
|
47
|
+
/** Handler-acquisition requirements of the implementations a surface acquires, or of a union of them. */
|
|
48
|
+
export type BuildContext<App, Hidden = never> = App extends Implementation<infer G, any, any, infer RX> ? [ServedNames<G, Hidden>] extends [never] ? never : RX : never;
|
|
50
49
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Predicate } from "effect";
|
|
2
2
|
//#region src/internal/implementation.ts
|
|
3
3
|
/**
|
|
4
4
|
* An acquired handler record for a group.
|
|
@@ -30,8 +30,8 @@ var Implementation = class Implementation {
|
|
|
30
30
|
* per-request services while assembling routes.
|
|
31
31
|
*/
|
|
32
32
|
const dispatch = (group, action, handlers, before) => {
|
|
33
|
-
const handle = handlers[action.name];
|
|
34
|
-
if (handle
|
|
33
|
+
const handle = Object.hasOwn(handlers, action.name) ? handlers[action.name] : void 0;
|
|
34
|
+
if (!Predicate.isFunction(handle)) throw new Error(`No handler for ${group.name}.${action.name}`);
|
|
35
35
|
const attributes = {
|
|
36
36
|
"action.group": group.name,
|
|
37
37
|
"action.name": action.name,
|
package/docs/Action.md
CHANGED
|
@@ -7,18 +7,17 @@ metadata. An action holds no behavior; handlers are bound by `ActionGroup.implem
|
|
|
7
7
|
|
|
8
8
|
Import `@gjermundgaraba/effect-actions/Action`.
|
|
9
9
|
|
|
10
|
-
| Export | Purpose
|
|
11
|
-
| ------------------------------------------ |
|
|
12
|
-
| `make(name, options)` | Define a pure contract; literal names, access and
|
|
13
|
-
| `Action`, `Any`, `Options` | Concrete contracts, erased contracts and construction options.
|
|
14
|
-
| `Codec`, `Handler`, `Access`, `McpOptions` | Service-free codecs, typed handlers, read/write classification and tool metadata.
|
|
10
|
+
| Export | Purpose |
|
|
11
|
+
| ------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
12
|
+
| `make(name, options)` | Define a pure contract; literal names, access and MCP exclusion stay typed. |
|
|
13
|
+
| `Action`, `Any`, `Options` | Concrete contracts, erased contracts and construction options. |
|
|
14
|
+
| `Codec`, `Handler`, `Access`, `McpOptions` | Service-free codecs, typed handlers, read/write classification and tool metadata. |
|
|
15
15
|
|
|
16
16
|
| Option | Meaning |
|
|
17
17
|
| ---------------------------------- | ---------------------------------------------------------------------------- |
|
|
18
18
|
| `description`, `success`, `access` | Required description, success codec and `"read"` / `"write"` classification. |
|
|
19
19
|
| `input` | Optional codec; omission means an empty object. |
|
|
20
20
|
| `errors` | Declared error codecs; defaults to none. |
|
|
21
|
-
| `http` | Defaults to enabled; `false` removes routes and client methods. |
|
|
22
21
|
| `mcp` | Defaults to enabled; `false` removes tools, otherwise accepts `McpOptions`. |
|
|
23
22
|
|
|
24
23
|
MCP options: `name` defaults to the action name; `readOnly` to `access === "read"`;
|
|
@@ -65,12 +64,11 @@ export const WhoAmI = Action.make("whoAmI", {
|
|
|
65
64
|
access: "read",
|
|
66
65
|
});
|
|
67
66
|
|
|
68
|
-
//
|
|
67
|
+
// A tool only: its group is left out of the HTTP binding (see ActionHttp.md).
|
|
69
68
|
export const ListChanges = Action.make("listChanges", {
|
|
70
69
|
description: "List renames in your tenant, oldest first.",
|
|
71
70
|
success: Schema.Struct({ changes: Schema.Array(Schema.String) }),
|
|
72
71
|
access: "read",
|
|
73
|
-
http: false,
|
|
74
72
|
mcp: { name: "list_changes" },
|
|
75
73
|
});
|
|
76
74
|
|
|
@@ -90,8 +88,10 @@ export const Double = Action.make("double", {
|
|
|
90
88
|
- `errors` is a list of schemas, default none. Each keeps its own `httpApiStatus` annotation; an unannotated error is served as HTTP 500. Group-level `errors` are appended to every action of the group (see [ActionGroup.md](ActionGroup.md)).
|
|
91
89
|
- A handler may fail only with the declared errors. Anything else is a defect.
|
|
92
90
|
- `access` is `"read"` or `"write"` and is required. `make` also checks it at runtime, so a caller the compiler never sees cannot define an action no rule classifies. It stays a literal on the action, so a rule may switch on it at the type level. It is authorization metadata for a surface's `before` hook (see [guarantees.md](guarantees.md)); the library itself authorizes nothing. Its only built-in uses are default MCP hints and span/log annotations; adapters never enforce an authorization rule from it.
|
|
93
|
-
- `access` is independent of `mcp`.
|
|
94
|
-
-
|
|
91
|
+
- `access` is independent of `mcp`. An action hidden from MCP (`mcp: false`) still has one, and an action may set `access: "write"` with `mcp: { readOnly: true }` if the tool hint should say something else. Derive authorization from `access`, never from a tool hint.
|
|
92
|
+
- An action has no HTTP switch. HTTP serves every action of every group it binds; to keep an action off HTTP, put it in a group the HTTP binding leaves out. `ActionCli` runs any action locally.
|
|
93
|
+
- `mcp: false` removes the tool. The action's type is hidden from MCP only when the options' type has a required `mcp: false`, such as a literal `mcp: false` in the object passed to `make`. Options that may serve it type it as served, so MCP layers keep its handler's requirements: a conditional spread of `{ mcp: false }`, `false | undefined`, a flag-driven value such as `enabled ? {} : false`, or an `Options` value whose `mcp` is optional.
|
|
94
|
+
- `make` accepts only the keys `Options` declares. An unknown key, such as a stale `http`, is a compile error.
|
|
95
95
|
- MCP input must have an object-root JSON Schema, an identified or recursive root included. Scalar or array input is fine for HTTP and for a native Toolkit, but the native MCP server refuses it when an `ActionMcp` layer is built. Success and error schemas may be any shape.
|
|
96
96
|
- `mcp.name` is the tool name, matches `[A-Za-z0-9_-]+` except `then`, is at most 128 characters, unique within the group and within any Toolkit or MCP projection that serves the group. Default is the action name.
|
|
97
97
|
- Hint defaults: `readOnly: access === "read"`, `destructive: !readOnly`, `idempotent: false`, `openWorld: true`. Hints are metadata for the model. They do not enforce authorization, approval, or retries.
|
|
@@ -105,4 +105,5 @@ export const Double = Action.make("double", {
|
|
|
105
105
|
- Type error `Effect<..., X, ...> is not assignable` in `implement`: the handler fails with an undeclared error `X`. Add it to `errors` or handle it.
|
|
106
106
|
- Handler receives a string where a number was expected: the schema is `Schema.String`, not a transforming codec such as `Schema.FiniteFromString`.
|
|
107
107
|
- `Property 'access' is missing` at `make`: every action declares `"read"` or `"write"`. There is no default.
|
|
108
|
+
- `Type 'false' is not assignable to type 'never'` on `http` at `make`: the option is gone. Delete it; to keep the action off HTTP, move it to a group the HTTP binding leaves out.
|
|
108
109
|
- A tool shows `readOnlyHint: false` for a read: the action sets `mcp: { readOnly: false }` explicitly, which wins over `access`.
|
package/docs/ActionCatalog.md
CHANGED
|
@@ -7,15 +7,15 @@ Built from contracts alone: no implementation, service or server is involved.
|
|
|
7
7
|
|
|
8
8
|
Import `@gjermundgaraba/effect-actions/ActionCatalog`.
|
|
9
9
|
|
|
10
|
-
`make(...groups)` returns `Catalog`: `version: "
|
|
10
|
+
`make(...groups)` returns `Catalog`: `version: "4"` and an ordered `actions` array of `Entry`.
|
|
11
11
|
|
|
12
|
-
| Entry field | Meaning
|
|
13
|
-
| ------------------------------------ |
|
|
14
|
-
| `id`, `group`, `name`, `description` | Stable `<group>.<action>` identity and contract metadata.
|
|
15
|
-
| `
|
|
16
|
-
| `input`, `success` | Standalone draft 2020-12 JSON Schema objects, not wrappers.
|
|
17
|
-
| `errors` | JSON Schema objects for own and inherited group errors.
|
|
18
|
-
| `httpSchemaErrors` | JSON Schema objects for HTTP policy errors; empty
|
|
12
|
+
| Entry field | Meaning |
|
|
13
|
+
| ------------------------------------ | ------------------------------------------------------------------- |
|
|
14
|
+
| `id`, `group`, `name`, `description` | Stable `<group>.<action>` identity and contract metadata. |
|
|
15
|
+
| `mcp` | Resolved MCP enablement, name and hints. |
|
|
16
|
+
| `input`, `success` | Standalone draft 2020-12 JSON Schema objects, not wrappers. |
|
|
17
|
+
| `errors` | JSON Schema objects for own and inherited group errors. |
|
|
18
|
+
| `httpSchemaErrors` | JSON Schema objects for HTTP policy errors; empty without a policy. |
|
|
19
19
|
|
|
20
20
|
Exported types are `Catalog` and `Entry`.
|
|
21
21
|
|
|
@@ -43,7 +43,7 @@ Console.log(
|
|
|
43
43
|
- Each schema root includes its own `$schema` and, where needed, `$defs`. Equal identifiers across entries or fields cannot replace each other; recursive references resolve locally.
|
|
44
44
|
- The catalog contains JSON Schema only. Use Effect's `SchemaRepresentation` directly when native schema persistence or revival is needed.
|
|
45
45
|
- `errors` includes group-level errors. HTTP schema-policy errors are separate in `httpSchemaErrors`. Surface errors belong to bindings and are not listed.
|
|
46
|
-
- `access` is omitted; read it from the contract when needed.
|
|
46
|
+
- `access` is omitted; read it from the contract when needed. Actions hidden from MCP are included. Presence grants no authorization or proof that an endpoint is mounted; the host decides what to publish.
|
|
47
47
|
- No search, catalog HTTP endpoint, or TypeScript code generation is provided.
|
|
48
48
|
|
|
49
49
|
## Failure modes
|
package/docs/ActionCli.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# ActionCli
|
|
2
2
|
|
|
3
3
|
Native Effect CLI commands that run handlers in-process. The host provides every service;
|
|
4
|
-
nothing is fetched over HTTP.
|
|
4
|
+
nothing is fetched over HTTP. Any action runs locally, whether or not HTTP or MCP serves it.
|
|
5
5
|
|
|
6
6
|
## API
|
|
7
7
|
|
|
8
8
|
Import `@gjermundgaraba/effect-actions/ActionCli`.
|
|
9
9
|
|
|
10
|
-
| API | Purpose
|
|
11
|
-
| ------------------------------ |
|
|
12
|
-
| `command(app, name, options?)` | One selected action as a native Effect `Command`.
|
|
13
|
-
| `group(app, options?)` | Every action under a group command
|
|
14
|
-
| `Options`, `GroupOptions` | Configuration for single and aggregate commands.
|
|
10
|
+
| API | Purpose |
|
|
11
|
+
| ------------------------------ | ------------------------------------------------- |
|
|
12
|
+
| `command(app, name, options?)` | One selected action as a native Effect `Command`. |
|
|
13
|
+
| `group(app, options?)` | Every action under a group command. |
|
|
14
|
+
| `Options`, `GroupOptions` | Configuration for single and aggregate commands. |
|
|
15
15
|
|
|
16
16
|
| Option | Meaning |
|
|
17
17
|
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
@@ -56,7 +56,7 @@ Command.runWith(command, { version: "0.1.0" })(process.argv.slice(2)).pipe(
|
|
|
56
56
|
|
|
57
57
|
## Rules
|
|
58
58
|
|
|
59
|
-
- `command(app, name)` selects one action by name from the bound group; the name is checked at the type level. `group(app)` puts every action under the group command, including actions with `
|
|
59
|
+
- `command(app, name)` selects one action by name from the bound group; the name is checked at the type level. `group(app)` puts every action under the group command, including actions with `mcp: false` and groups no HTTP binding serves.
|
|
60
60
|
- Default syntax is `--input '<json>'` or `--input-file <path>` carrying the entire encoded input: nested objects, arrays, scalars. The file is read and JSON-parsed by the native `Flag.FileSchema`. Both are decoded by the native parser; if both are given, the file is used. Omitting both decodes `{}` separately on every invocation; result identity follows the codec's behavior. An action that requires input then fails with a `SchemaError`, exactly as a `parameters` command's mapped input does.
|
|
61
61
|
- `parameters` and `input` are supplied together or not at all. With them, the command has explicit native flags and arguments and neither `--input` nor `--input-file`, so its syntax does not change when the schema changes.
|
|
62
62
|
- `input(parsed)` returns encoded JSON. The action schema decodes it before dispatch; a mismatch is a `SchemaError` at runtime. Action fields are never turned into flags automatically.
|
package/docs/ActionCliClient.md
CHANGED
|
@@ -9,8 +9,8 @@ Import `@gjermundgaraba/effect-actions/ActionCliClient`.
|
|
|
9
9
|
|
|
10
10
|
| API | Purpose |
|
|
11
11
|
| ------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
12
|
-
| `command(http, groupName, actionName, options?)` | Call one
|
|
13
|
-
| `group(http, groupName, options?)` | Every
|
|
12
|
+
| `command(http, groupName, actionName, options?)` | Call one action of the binding through a native Effect `Command`. |
|
|
13
|
+
| `group(http, groupName, options?)` | Every action of a bound group under one command. |
|
|
14
14
|
| `Options`, `GroupOptions`, `Connection` | Single/aggregate command configuration and native HTTP connection options. |
|
|
15
15
|
|
|
16
16
|
Single-command options: `name`, `render`, paired `parameters`/`input`, and `connection`.
|
|
@@ -49,7 +49,7 @@ Authentication: `connection: { transformClient: HttpClient.mapRequest(HttpClient
|
|
|
49
49
|
|
|
50
50
|
## Rules
|
|
51
51
|
|
|
52
|
-
- Selectors resolve from `http.groups` only. `groupName` must be a bound group; `actionName` must be an action of that group
|
|
52
|
+
- Selectors resolve from `http.groups` only. `groupName` must be a bound group; `actionName` must be an action of that group. Runtime guards reject dynamically supplied strings that the types would not accept.
|
|
53
53
|
- The host provides `HttpClient` and its configuration. Endpoint selection, credentials, and storage are the host's; nothing is inferred from action arguments.
|
|
54
54
|
- Input is decoded by the action schema before dispatch, then passed to the native client at its normal codec boundary. Do not pre-encode values.
|
|
55
55
|
- Input syntax (`--input`, `--input-file`, or `parameters`) and output (`render` and its `--json` flag) are exactly as in `ActionCli`.
|
|
@@ -57,7 +57,7 @@ Authentication: `connection: { transformClient: HttpClient.mapRequest(HttpClient
|
|
|
57
57
|
|
|
58
58
|
## Failure modes
|
|
59
59
|
|
|
60
|
-
- Type error on `actionName`: the action is
|
|
60
|
+
- Type error on `actionName`: the action is not in that group, or the group is not bound in this `Http`.
|
|
61
61
|
- `HttpClient` missing at runtime: provide `NodeHttpClient.layerUndici` (or `FetchHttpClient.layer`) to the runtime.
|
|
62
62
|
- Connection refused: `connection.baseUrl` is absent or wrong. It has no default.
|
|
63
63
|
- 401 from the server: add `transformClient` to `connection`. The command adds no headers on its own.
|
package/docs/ActionGroup.md
CHANGED
|
@@ -93,7 +93,7 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
93
93
|
|
|
94
94
|
- `name` matches `[A-Za-z0-9_-]+`, cannot be `then`, and must be unique within one `ActionHttp.make`. Action names must be unique within the group. MCP tool names must be unique within the group and within each Toolkit or MCP projection that serves it. Duplicates fail at `make`.
|
|
95
95
|
- Group `errors` are appended to each action's own `errors`, so every handler of the group may fail with them. A failure that only the surface produces belongs on the adapter instead (`ActionHttp.make`'s `errors`, `ActionMcp`'s and `ActionToolkit`'s `errors`), not here.
|
|
96
|
-
- `implement` takes a complete record or an Effect producing one. Every action must have a handler; missing keys are compile errors. A record that still lacks one at runtime (plain JavaScript, a cast) throws at `implement`; a builder's record fails the adapter layer's build. Nothing is served with a handler missing. Additional keys are not dispatched. It takes nothing else.
|
|
96
|
+
- `implement` takes a complete plain record or an Effect producing one. Every action must have a handler; missing keys are compile errors. A handler is an own-property function of the record: an inherited one, such as a class instance's method, does not count. A record that still lacks one at runtime (plain JavaScript, a cast: a missing key, `undefined`, any other non-function, an inherited method) throws at `implement`; a builder's record fails the adapter layer's build. Nothing is served with a handler missing. Handlers are called without a receiver. Additional keys are not dispatched. It takes nothing else.
|
|
97
97
|
- The builder Effect runs once per adapter layer that serves the implementation, in that layer's scope. An implementation served by HTTP and MCP is built twice. Acquire shared state in a Layer you provide to the adapters, not in the builder.
|
|
98
98
|
- Services yielded in the builder are build-time requirements (`RX`). Services yielded in a handler are request-time requirements (`R` of the handler). Adapters keep these separate in their types. Use distinct tags for each kind; never provide a request-identity tag at startup (see [guarantees.md](guarantees.md)).
|
|
99
99
|
- `Implementation` is nominal. Spreading its properties does not produce an implementation. Each `implement` call is a separate binding.
|
|
@@ -121,6 +121,6 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
121
121
|
- Handler compiles but returns an error not in `errors`: type error on the handler's error channel. Declare it on the action or the group.
|
|
122
122
|
- `Expected 1 arguments, but got 2` at `implement`: the second options object is gone. Move `before` to the adapter that serves the group.
|
|
123
123
|
- `Parameter 'failure' implicitly has an 'any' type` in a standalone policy constant: a `make` that reads the failure is typed only inline. Annotate the parameter as `HttpApiError.HttpApiSchemaError` (type import from `effect/unstable/httpapi`), or annotate the constant as `ActionGroup.SchemaErrorPolicy<typeof Invalid, typeof Internal>`.
|
|
124
|
-
- `Missing handlers for group "<group>": <actions>` thrown at `implement`, or failing the layer build: the handler record
|
|
124
|
+
- `Missing handlers for group "<group>": <actions>` thrown at `implement`, or failing the layer build: the handler record has no own-property function for those actions. Add them to the record itself, not to a prototype.
|
|
125
125
|
- A service is resolved once and shared across requests when it should be per request: it was yielded in the builder. Move the `yield*` into the handler.
|
|
126
126
|
- `Duplicate contract group` thrown by `contracts`: two groups share a name, so their keys would collide. Rename one.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -23,9 +23,8 @@ Exported types: `Options`, `LayerOptions`, `Http`, `Api`.
|
|
|
23
23
|
| `make`: `errors` | Surface error codecs declared on every endpoint; defaults to none. |
|
|
24
24
|
| `layer`: `before` | Effectful hook receiving the selected `Action.Any`, after successful input decoding and before its handler. Failures must belong to the binding's `errors`. |
|
|
25
25
|
|
|
26
|
-
Layer failures and startup requirements come from the supplied implementations
|
|
27
|
-
|
|
28
|
-
requirements; router/platform services are also required. No request identity is supplied at startup.
|
|
26
|
+
Layer failures and startup requirements come from the supplied implementations. Every
|
|
27
|
+
handler's and the hook's request services remain router request requirements; router/platform services are also required. No request identity is supplied at startup.
|
|
29
28
|
|
|
30
29
|
Route shape: `POST <apiPath>/<group>/<action>`. Operation ID: `<group>.<action>`.
|
|
31
30
|
|
|
@@ -88,11 +87,12 @@ export const greeting = Effect.gen(function* () {
|
|
|
88
87
|
|
|
89
88
|
## Rules
|
|
90
89
|
|
|
90
|
+
- HTTP serves every action of every group passed to `make`; an action has no HTTP switch. To keep an action off HTTP, put it in a group left out of `make` and serve that group elsewhere (MCP, Toolkit, CLI).
|
|
91
91
|
- `apiPath` has no default. Group names must be unique within one `make`. Action names need only be unique within their group; equal action names in different groups do not collide.
|
|
92
92
|
- `errors` declares the failures the surface around these endpoints answers with rather than a handler: authentication, authorization, rate limiting, upstream unavailability. They are added to every endpoint's error schemas, so `HttpApiClient`, `ActionCliClient` and the in-memory `Testing.httpClient` decode them as typed failures, and they appear in OpenAPI on every operation. A schema an action already declares is not repeated.
|
|
93
93
|
- Schemas reachable from one endpoint must have distinct `_tag`s: the client decodes a response by trying the schemas declared for its status, and two errors may share a status. Effect unions them per status.
|
|
94
94
|
- `errors` changes only what is declared. Nothing produces them: the middleware that renders those responses must encode a body that matches the schema, or the client sees a decode error again. Handlers cannot fail with them.
|
|
95
|
-
- `Http.layer(apps, options?)` requires each implementation to reference the exact group object passed to `make`; reconstructing an equal-looking group is not sufficient. Each group may occur only once per call. Only supplied implementations are mounted, and a group
|
|
95
|
+
- `Http.layer(apps, options?)` requires each implementation to reference the exact group object passed to `make`; reconstructing an equal-looking group is not sufficient. Each group may occur only once per call. Only supplied implementations are mounted, and a group without actions is not built. Options are optional.
|
|
96
96
|
- `before` runs after successful native input decoding and before the selected handler. Invalid input skips the hook and handler. A refusal uses the binding's declared error schema and `httpApiStatus`; its services join handler request requirements. Admission that must precede decoding belongs in outer native HTTP middleware.
|
|
97
97
|
- Middleware and the hook are per layer call. Groups that need different middleware or a different policy go in separate `Http.layer` calls, merged with `Layer.mergeAll`.
|
|
98
98
|
- `ActionHttp` sets no response headers of its own. The host owns cache policy; `Authentication.middleware` marks its responses `cache-control: no-store`.
|
|
@@ -101,19 +101,19 @@ export const greeting = Effect.gen(function* () {
|
|
|
101
101
|
- `Http.api` is a plain `HttpApi`. Anything Effect can do with an `HttpApi` works: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `HttpApi.addHttpApi` to combine with other APIs, `HttpApiClient.make`.
|
|
102
102
|
- `Http.openApi(path?)` is `OpenApi.fromApi(Http.api)` as one `GET` route, `<apiPath>/openapi.json` unless a path is given. It documents every bound group, not only the served ones. It is a plain route: middleware provided to its layer covers it, and nothing covers it otherwise.
|
|
103
103
|
- Client calls always take `{ payload }`. No-input actions take `{ payload: {} }`. Pass `null` or `undefined` only when the codec accepts it. There is no flat client.
|
|
104
|
-
- Client effects fail with the declared errors, the group's policy errors, the binding's `errors`, `SchemaError` for local codec failures, and native `HttpClientError`.
|
|
104
|
+
- Client effects fail with the declared errors, the group's policy errors, the binding's `errors`, `SchemaError` for local codec failures, and native `HttpClientError`.
|
|
105
105
|
- Add authentication headers with the native `transformClient` option. Use `HttpApiClient.makeWith` for custom error or service channels. Native per-call response modes are available.
|
|
106
106
|
- Wire format without a policy: input failure is an empty 400, success is the encoded body, a declared error is its JSON encoding with its `httpApiStatus`, an encoding failure is an empty 400, a defect is an empty 500. Full table in [guarantees.md](guarantees.md).
|
|
107
107
|
- Each handler runs in a span named `<group>.<action>`, a child of the request span, attributed with `action.group`, `action.name` and `action.access`; its log lines carry the same annotations. The hook, decoding and encoding are outside it, in the request span.
|
|
108
108
|
|
|
109
109
|
## Failure modes
|
|
110
110
|
|
|
111
|
-
- Route returns 404: the implementation was never passed to `Http.layer`, or
|
|
111
|
+
- Route returns 404: the implementation was never passed to `Http.layer`, or its group is not bound by `make`, or the path lacks the group segment.
|
|
112
112
|
- Type error listing `HttpRouter.Request.From<"Requires", CurrentActor>` as unsatisfied: a handler yields a request service and no middleware provides it. Wrap that `Http.layer` call with the middleware's `.layer`.
|
|
113
113
|
- Two groups share a name: `make` throws. Rename one.
|
|
114
114
|
- `Implementation of group "x" is not served by this adapter`: `Http.layer` received an implementation whose group was not passed to `ActionHttp.make`. Implement the exact shared group object bound by `make`, or add a genuinely new group to the binding. Matching names and schemas do not establish identity.
|
|
115
115
|
- `Duplicate implementation group: <name>`: a single `Http.layer` call received more than one implementation of the same group. Pass one implementation per group.
|
|
116
|
-
- Client method missing for an action:
|
|
116
|
+
- Client method missing for an action: its group is not bound by `make`.
|
|
117
117
|
- Empty 400 on a valid-looking request: input did not decode. Set a `schemaError` policy on the group to get a typed body, and check `Content-Type: application/json`.
|
|
118
118
|
- 415: wrong or missing content type.
|
|
119
119
|
- `HttpClientError: Decode error (401 POST ...)` from a typed client: the surface answered with a status no endpoint declares. Add that error schema to `ActionHttp.make`'s `errors`.
|
package/docs/ActionHttpClient.md
CHANGED
|
@@ -7,17 +7,21 @@ page. It is Effect's native `HttpApiClient` over `fetch`, built once; each call
|
|
|
7
7
|
|
|
8
8
|
Import `@gjermundgaraba/effect-actions/ActionHttpClient`.
|
|
9
9
|
|
|
10
|
-
| API | Purpose
|
|
11
|
-
| ------------------------- |
|
|
12
|
-
| `promise(http, options?)` | `client.<group>.<action>(input)` for every
|
|
13
|
-
| `Options` | Where and how requests are sent.
|
|
14
|
-
| `Client`, `Method` | The client's type, derived from the binding, and one action's method.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
|
10
|
+
| API | Purpose |
|
|
11
|
+
| ------------------------- | --------------------------------------------------------------------- |
|
|
12
|
+
| `promise(http, options?)` | `client.<group>.<action>(input)` for every action of the binding. |
|
|
13
|
+
| `Options` | Where and how requests are sent. |
|
|
14
|
+
| `Client`, `Method` | The client's type, derived from the binding, and one action's method. |
|
|
15
|
+
|
|
16
|
+
The options are the native `HttpApiClient.make` options except `transformResponse`, plus `fetch`.
|
|
17
|
+
`transformResponse` may change a call's success, failure or required services, which neither
|
|
18
|
+
the method types nor a Promise can follow; wrap `fetch` or use `transformClient` instead.
|
|
19
|
+
|
|
20
|
+
| Option | Meaning |
|
|
21
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `baseUrl` | What routes are resolved against, such as `https://api.example.com`. Omitted: relative routes (the page's origin in a browser). |
|
|
23
|
+
| `transformClient` | Wraps the native `HttpClient`. A bearer token: `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. |
|
|
24
|
+
| `fetch` | The transport. Defaults to the global `fetch`, looked up on each call. |
|
|
21
25
|
|
|
22
26
|
A method takes the action's decoded input and resolves with its decoded success. An action
|
|
23
27
|
whose input may be empty (no `input`, or only optional fields) may be called without an argument.
|
|
@@ -25,14 +29,16 @@ whose input may be empty (no `input`, or only optional fields) may be called wit
|
|
|
25
29
|
## Canonical
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
|
-
import { HttpClientError } from "effect/unstable/http";
|
|
32
|
+
import { HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http";
|
|
29
33
|
import * as ActionHttpClient from "@gjermundgaraba/effect-actions/ActionHttpClient";
|
|
30
34
|
import { Http, UserNotFound } from "./contracts.js";
|
|
31
35
|
|
|
32
36
|
// Promises in, Promises out: for code that does not run Effects, such as a browser page.
|
|
37
|
+
// The options are the native `HttpApiClient.make` options plus `fetch`; a bearer token is
|
|
38
|
+
// a `transformClient`.
|
|
33
39
|
const client = ActionHttpClient.promise(Http, {
|
|
34
40
|
baseUrl: "http://127.0.0.1:3000",
|
|
35
|
-
|
|
41
|
+
transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken("alice")),
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
export const userName = async (id: string): Promise<string> => {
|
|
@@ -52,8 +58,9 @@ export const userName = async (id: string): Promise<string> => {
|
|
|
52
58
|
};
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
Other headers
|
|
56
|
-
|
|
61
|
+
Other headers also go through `transformClient` (`HttpClient.mapRequest(HttpClientRequest.setHeader("x-agent", agent))`).
|
|
62
|
+
Reacting to a response every call may meet (a proxy's 401), or a token read per call, goes through
|
|
63
|
+
`fetch`: `fetch: (input, init) => { const request = new Request(input, init); request.headers.set("authorization", "Bearer " + readToken()); return fetch(request); }`.
|
|
57
64
|
|
|
58
65
|
## Rules
|
|
59
66
|
|
|
@@ -61,7 +68,7 @@ Other headers, or reacting to a response every call may meet (a proxy's 401), go
|
|
|
61
68
|
- Anything the contract does not account for rejects with Effect's own error. `HttpClientError`: the server could not be reached (`response` is `undefined`), or answered with a status no schema declares, or with a body that could not be read. `SchemaError`: the input did not encode, or the success body did not decode.
|
|
62
69
|
- The library adds no error type of its own and interprets no status. Which failures mean "signed out" or "try again" is the caller's decision.
|
|
63
70
|
- Nothing is retried. A rejected write may or may not have happened; only a declared error says what the server did.
|
|
64
|
-
-
|
|
71
|
+
- Every action of a bound group has a method. A group without actions has no namespace.
|
|
65
72
|
- The client holds no connections or timers. Making one is synchronous and sends nothing.
|
|
66
73
|
- A method is not an Effect. Code that runs Effects uses `HttpApiClient.make(Http.api)` directly (see [ActionHttp.md](ActionHttp.md)); its methods take `{ payload }`.
|
|
67
74
|
|
|
@@ -70,4 +77,4 @@ Other headers, or reacting to a response every call may meet (a proxy's 401), go
|
|
|
70
77
|
- Rejects with `HttpClientError` whose `reason._tag` is `StatusCodeError` for a 401 or 403: the surface answered with an error the binding does not declare. Add it to `ActionHttp.make`'s `errors`, and have the middleware encode it.
|
|
71
78
|
- Rejects with `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: `baseUrl` is omitted, and there is no page to resolve relative routes against. Set `baseUrl`.
|
|
72
79
|
- A stubbed global `fetch` is not used: `fetch` was passed as an option, which takes precedence.
|
|
73
|
-
- Property does not exist on the client: the action
|
|
80
|
+
- Property does not exist on the client: the action's group is not bound in this `Http`.
|
package/docs/ActionMcp.md
CHANGED
|
@@ -142,12 +142,12 @@ Layer.launch(layer).pipe(
|
|
|
142
142
|
|
|
143
143
|
## Rules
|
|
144
144
|
|
|
145
|
-
- Both transports serve MCP 2026-07-28 and no other revision; there is no `protocols` option. Over HTTP the endpoint is stateless: no initialize handshake, no session, and every request stands alone. Effect owns version checks and rejects any other revision.
|
|
145
|
+
- Both transports serve MCP 2026-07-28 and no other revision; there is no `protocols` option. Over HTTP the endpoint is stateless: no initialize handshake, no session, and every request stands alone. Effect owns version checks and rejects any other revision. Stdio has no sessions, so it could serve an older host, but it refuses one deliberately, for uniformity: a client that works over one transport works over the other.
|
|
146
146
|
- `path` has no default. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form, whatever revision is negotiated.
|
|
147
147
|
- Requests reaching the native MCP handler with an `Origin` header receive **403** unless that exact origin is listed in `allowedOrigins`. Requests without `Origin` pass this check. Authentication middleware wrapping the endpoint runs first and may reject the request before native Origin validation; the allowlist does not protect authentication from untrusted-origin requests.
|
|
148
148
|
- `allowedOrigins` is an Origin allowlist, not CORS configuration. Cross-origin browser clients also need outer CORS middleware or a proxy to handle preflight and add response headers. Without it, an allowed-origin `OPTIONS` request receives **405** and even a successful `POST` has no `Access-Control-Allow-Origin`. Keep preflight outside authentication and apply CORS headers to refusals too.
|
|
149
149
|
- An endpoint is one route. Middleware provided to `layerHttp` covers all of its tools. To serve tools under different middleware, mount them on different paths with separate `layerHttp` calls.
|
|
150
|
-
- Only MCP-enabled actions become tools, under `mcp.name` with the resolved hints. A group with no tools is not built.
|
|
150
|
+
- Only MCP-enabled actions become tools, under `mcp.name` with the resolved hints. A group with no tools is not built. The layer's types drop an action's requirements only when its type is certainly hidden (a literal `mcp: false`, see [Action.md](Action.md)); an `mcp` that may be `false` at runtime keeps them.
|
|
151
151
|
- Every MCP-enabled action must have object-root input; the native server refuses anything else when the layer is built. Omit `input` for a tool with no arguments.
|
|
152
152
|
- Success is `structuredContent: { value: <encoded success> }`. A declared error is an `isError` result whose text content is the error's JSON encoding, the same bytes HTTP sends as the body, and no `structuredContent`.
|
|
153
153
|
- Invalid arguments and unencodable results are answered by the native `McpServer`: an `isError` result with a message for the model. Group `schemaError` policies do not apply.
|
package/docs/ActionToolkit.md
CHANGED
|
@@ -51,7 +51,7 @@ With a model: pass `binding.toolkit` as `toolkit` to `LanguageModel.generateText
|
|
|
51
51
|
|
|
52
52
|
## Rules
|
|
53
53
|
|
|
54
|
-
- Selects MCP-enabled actions only, named by `mcp.name`, with resolved hints. Groups with no selected tools are not built. `make(apps, options?)` accepts a readonly implementation collection; options may be omitted.
|
|
54
|
+
- Selects MCP-enabled actions only, named by `mcp.name`, with resolved hints. Groups with no selected tools are not built. The toolkit's type follows `ActionMcp`'s rule: only an action whose `mcp` type is exactly `false` has no tool; one whose `mcp` may be `false` at runtime is typed as a tool with its handler's requirements. `make(apps, options?)` accepts a readonly implementation collection; options may be omitted.
|
|
55
55
|
- Successes are the action's native values. There is no `{ value }` wrapper. Declared failures are returned as native tool results (`failureMode: "return"`), not raised.
|
|
56
56
|
- `tools.handle(name, encodedInput)` takes encoded arguments and returns an Effect producing a result stream. The handler starts while that stream is constructed, so request services must be provided around the entire `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, not only around the stream.
|
|
57
57
|
- `binding.layer` acquires each implementation once, in the layer's scope. Build-time requirements belong to the whole selected implementation.
|
package/docs/CONTEXT.md
CHANGED
|
@@ -5,7 +5,7 @@ use these words with these meanings.
|
|
|
5
5
|
|
|
6
6
|
## Terms
|
|
7
7
|
|
|
8
|
-
- **Action**: one contract. A name, a description, an input schema, a success schema, declared error schemas, authorization metadata (`access`), and
|
|
8
|
+
- **Action**: one contract. A name, a description, an input schema, a success schema, declared error schemas, authorization metadata (`access`), and MCP metadata (`mcp`). Holds no behavior. Made by `Action.make`.
|
|
9
9
|
- **Access**: `"read"` or `"write"`, required on every action and kept as a literal. States what the action does to its resource. Authorization metadata read by a pre-handler hook, independent of the MCP `readOnly` hint, which defaults from it.
|
|
10
10
|
- **Group**: a named, ordered set of actions plus group-level errors and an optional schema-error policy. Adapters serve groups, clients are organized by group, and the group name is the OpenAPI tag. Made by `ActionGroup.make`.
|
|
11
11
|
- **Contract**: an action or a group. Pure data; safe to import anywhere, including browsers.
|
|
@@ -22,11 +22,10 @@ use these words with these meanings.
|
|
|
22
22
|
- **Schema-error policy**: a group option answering Effect's `HttpApiSchemaError` with a declared policy error and its status: `invalid` when the request did not decode, `internal` when the handler's result did not encode. The library decides which applies. HTTP only.
|
|
23
23
|
- **Policy error**: an error a schema-error policy may answer with. Part of the transport contract, never returnable by a handler.
|
|
24
24
|
- **Surface error**: an error declared on an adapter binding rather than on an action. Produced by middleware around the surface or by its pre-handler hook, never by a handler; declared so typed callers decode it.
|
|
25
|
-
- **Local-only action**: `http: false` and `mcp: false`. Reachable through `ActionCli` and listed in the catalog.
|
|
26
25
|
- **Tool**: the MCP or Toolkit projection of an MCP-enabled action, named by `mcp.name` and carrying its hints.
|
|
27
26
|
- **Endpoint**: one `ActionMcp.layerHttp` mount. One route, one middleware set, one tool registry.
|
|
28
27
|
- **Document**: the OpenAPI output of `Http.api`, produced by Effect (`OpenApi.fromApi`) and served by `Http.openApi`.
|
|
29
|
-
- **Promise client**: `ActionHttpClient.promise(Http)`, the native `HttpApiClient` as one Promise method per
|
|
28
|
+
- **Promise client**: `ActionHttpClient.promise(Http)`, the native `HttpApiClient` as one Promise method per action of the binding, for code that does not run Effects. It rejects with what the native client fails with.
|
|
30
29
|
- **Catalog**: the offline JSON description of groups produced by `ActionCatalog.make`, with standalone JSON Schemas for encoded values. Descriptive only.
|
|
31
30
|
- **Identity**: the per-request principal, provided by `Authentication.middleware` under an application-owned tag. Never provided at startup.
|
|
32
31
|
|
package/docs/guarantees.md
CHANGED
|
@@ -42,7 +42,7 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
42
42
|
|
|
43
43
|
- 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.
|
|
44
44
|
- Adapters validate only the namespace they serve. HTTP does not check tool names; MCP does not check route names.
|
|
45
|
-
- Each adapter builds only what it serves.
|
|
45
|
+
- Each adapter builds only what it serves. HTTP serves every action of a group it binds, so `Http.layer` builds every implementation it is given (except a group without actions); a group with no tools is not built by MCP or Toolkit.
|
|
46
46
|
|
|
47
47
|
## Observability
|
|
48
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|