@gjermundgaraba/effect-actions 0.2.0 → 0.4.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/README.md +5 -4
- package/dist/Action.d.mts +18 -6
- package/dist/Action.mjs +3 -1
- package/dist/ActionCli.d.mts +16 -9
- package/dist/ActionCli.mjs +4 -3
- package/dist/ActionCliClient.d.mts +9 -8
- package/dist/ActionCliClient.mjs +1 -1
- package/dist/ActionGroup.d.mts +16 -4
- package/dist/ActionGroup.mjs +5 -1
- package/dist/ActionHttp.d.mts +32 -12
- package/dist/ActionHttp.mjs +39 -14
- package/dist/ActionMcp.d.mts +24 -7
- package/dist/ActionMcp.mjs +13 -5
- package/dist/ActionToolkit.d.mts +25 -9
- package/dist/ActionToolkit.mjs +7 -2
- package/dist/Testing.mjs +1 -1
- package/dist/internal/actions.mjs +7 -1
- package/dist/internal/implementation.mjs +5 -2
- package/dist/internal/tools.mjs +13 -11
- package/docs/Action.md +35 -12
- package/docs/ActionCatalog.md +2 -1
- package/docs/ActionCli.md +25 -11
- package/docs/ActionGroup.md +25 -18
- package/docs/ActionHttp.md +44 -16
- package/docs/ActionMcp.md +23 -10
- package/docs/ActionToolkit.md +23 -7
- package/docs/Authentication.md +5 -2
- package/docs/CONTEXT.md +6 -2
- package/docs/README.md +5 -5
- package/docs/Testing.md +1 -1
- package/docs/guarantees.md +13 -4
- package/docs/setup.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ const Greet = Action.make("greet", {
|
|
|
18
18
|
description: "Greet someone by name.",
|
|
19
19
|
input: Schema.Struct({ name: Schema.String }),
|
|
20
20
|
success: Schema.String,
|
|
21
|
-
|
|
21
|
+
access: "read",
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
export const Actions = ActionGroup.make({ name: "greetings" }, Greet);
|
|
@@ -30,7 +30,7 @@ const app = Actions.implement({
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
export const routes = Layer.mergeAll(
|
|
33
|
-
Http.layer(app),
|
|
33
|
+
Http.layer({}, app),
|
|
34
34
|
ActionMcp.layerHttp(
|
|
35
35
|
{ protocols: [McpProtocol.v2026_07_28], name: "greetings", version: "1.0.0", path: "/mcp" },
|
|
36
36
|
app,
|
|
@@ -42,7 +42,7 @@ Serve `routes` with Effect's `HttpRouter` and you have `POST /api/actions/greeti
|
|
|
42
42
|
and an MCP tool named `greet` at `/mcp`. The same `app` is also:
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
const { toolkit, layer } = ActionToolkit.make(app); // native Effect AI Toolkit and its handler layer
|
|
45
|
+
const { toolkit, layer } = ActionToolkit.make({}, app); // native Effect AI Toolkit and its handler layer
|
|
46
46
|
const cli = ActionCli.group(app); // greetings greet --input '{"name":"Ada"}'
|
|
47
47
|
const remote = ActionCliClient.command(Http, "greetings", "greet"); // same command, over HTTP
|
|
48
48
|
const catalog = ActionCatalog.make(Actions); // offline JSON contract, no handlers acquired
|
|
@@ -61,8 +61,9 @@ const greeting = Effect.gen(function* () {
|
|
|
61
61
|
## Why
|
|
62
62
|
|
|
63
63
|
- Input, success, and errors are declared once. Routes, tools, commands, clients, OpenAPI, and the catalog are derived from that declaration, so they cannot disagree.
|
|
64
|
-
- A handler may fail only with the errors its action declares.
|
|
64
|
+
- A handler may fail only with the errors its action declares. Each surface binds one `before` hook that runs before every handler it serves — on HTTP before the payload is even decoded — so a policy such as scope enforcement is written once per surface and no action of that surface can skip it.
|
|
65
65
|
- The pieces are Effect's own. `Http.api` is a native `HttpApi`, so `OpenApi.fromApi`, Swagger, Scalar, and `HttpApiClient` work on it unchanged. MCP is Effect's native `McpServer`, one `Tool` per action, with no SDK runtime dependency.
|
|
66
|
+
- Every action states its `access` (`"read"` or `"write"`, required), so authorization reads the contract instead of a hand-maintained list of mutation names.
|
|
66
67
|
- Build-time services and per-request services are tracked separately in the types. Middleware is per group, so a public group and an authenticated group can share one mount path.
|
|
67
68
|
- Tests run in memory. Call the routes through a web handler with the same typed client, or drive the official MCP client, without opening a port.
|
|
68
69
|
|
package/dist/Action.d.mts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
declare namespace Action_d_exports {
|
|
3
|
-
export { Action, Any, Codec, Handler, McpOptions, Options, make };
|
|
3
|
+
export { Access, Action, Any, Codec, Handler, McpOptions, Options, make };
|
|
4
4
|
}
|
|
5
5
|
/** Any service-free schema. Only handlers may require services. */
|
|
6
6
|
export type Codec = Schema.Codec<unknown, unknown, never, never>;
|
|
7
|
+
/**
|
|
8
|
+
* What an action does to the resource it serves: `"read"` observes, `"write"`
|
|
9
|
+
* may change it. Authorization metadata, not an MCP hint.
|
|
10
|
+
*/
|
|
11
|
+
export type Access = "read" | "write";
|
|
7
12
|
/** MCP tool metadata; every field has a default derived from the action. */
|
|
8
13
|
export interface McpOptions {
|
|
9
14
|
/** Tool name; defaults to the action name. Must match `^[A-Za-z0-9_-]{1,128}$`. */
|
|
10
15
|
readonly name?: string;
|
|
11
|
-
/** `readOnlyHint`; defaults to `
|
|
16
|
+
/** `readOnlyHint`; defaults to `access === "read"`. */
|
|
12
17
|
readonly readOnly?: boolean;
|
|
13
18
|
/** `destructiveHint`; defaults to `!readOnly`, as the MCP spec only defines it for writes. */
|
|
14
19
|
readonly destructive?: boolean;
|
|
@@ -25,30 +30,37 @@ type ResolvedMcp<Name extends string, Mcp extends false | McpOptions | undefined
|
|
|
25
30
|
readonly openWorld: boolean;
|
|
26
31
|
};
|
|
27
32
|
/** What `make` needs to define an action. */
|
|
28
|
-
export interface Options<Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Http extends boolean = boolean, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
|
|
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> {
|
|
29
34
|
readonly description: string;
|
|
30
35
|
/** Omit for an action without arguments. */
|
|
31
36
|
readonly input?: Input;
|
|
32
37
|
readonly success: Output;
|
|
33
38
|
/** One schema per declared failure; each keeps its own HTTP status annotation. Defaults to none. */
|
|
34
39
|
readonly errors?: Errors;
|
|
40
|
+
/**
|
|
41
|
+
* What the action does to its resource. Required: an action nobody classified
|
|
42
|
+
* is the one a reviewer must check. Read by a surface's `before` hook; the
|
|
43
|
+
* library itself authorizes nothing.
|
|
44
|
+
*/
|
|
45
|
+
readonly access: Acc;
|
|
35
46
|
/** `false` hides the action from HTTP routes and clients. */
|
|
36
47
|
readonly http?: Http;
|
|
37
48
|
/** `false` hides the action from MCP; otherwise tool metadata. */
|
|
38
49
|
readonly mcp?: Mcp;
|
|
39
50
|
}
|
|
40
51
|
/** A pure contract: schemas and transport metadata. Implementations are bound by `ActionGroup.implement`. */
|
|
41
|
-
export interface Action<Name extends string, Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Http extends boolean = boolean, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
|
|
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> {
|
|
42
53
|
readonly name: Name;
|
|
43
54
|
readonly description: string;
|
|
44
55
|
readonly input: Input;
|
|
45
56
|
readonly success: Output;
|
|
46
57
|
readonly errors: Errors;
|
|
58
|
+
readonly access: Acc;
|
|
47
59
|
readonly http: Http;
|
|
48
60
|
readonly mcp: ResolvedMcp<Name, Mcp>;
|
|
49
61
|
}
|
|
50
62
|
/** Any action, with its schemas erased. */
|
|
51
|
-
export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>, boolean, false> | Action<string, Codec, Codec, ReadonlyArray<Codec>, boolean, McpOptions> | Action<string, Codec, Codec, ReadonlyArray<Codec>, boolean, undefined>;
|
|
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>;
|
|
52
64
|
/** Receives decoded input; may fail only with the declared errors. */
|
|
53
65
|
export type Handler<A extends Any, R = never> = (input: A["input"]["Type"]) => Effect.Effect<A["success"]["Type"], A["errors"][number]["Type"], R>;
|
|
54
66
|
/** An empty object schema that also produces the object root MCP requires. */
|
|
@@ -57,6 +69,6 @@ declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
|
|
|
57
69
|
* Define an action contract. Names are `[A-Za-z0-9_-]+`, other than `then`.
|
|
58
70
|
* Actions can be local-only by setting both transports to `false`.
|
|
59
71
|
*/
|
|
60
|
-
export declare function make<const Name extends string, Input extends Codec = typeof NoInput, Output extends Codec = never, const Errors extends ReadonlyArray<Codec> = [], const Http extends boolean = true, const Mcp extends false | McpOptions | undefined = undefined>(name: Name, options: Options<Input, Output, Errors, Http, Mcp>): Action<Name, Input, Output, Errors, Http, Mcp>;
|
|
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>;
|
|
61
73
|
//#endregion
|
|
62
74
|
export { Action_d_exports };
|
package/dist/Action.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import { Schema } from "effect";
|
|
|
5
5
|
const NoInput = Schema.Record(Schema.String, Schema.Never);
|
|
6
6
|
function make(name, options) {
|
|
7
7
|
assertName("action name", name);
|
|
8
|
-
const
|
|
8
|
+
const { access } = options;
|
|
9
|
+
const readOnly = options.mcp === false ? false : options.mcp?.readOnly ?? access === "read";
|
|
9
10
|
const mcp = options.mcp === false ? false : {
|
|
10
11
|
name: options.mcp?.name ?? name,
|
|
11
12
|
readOnly,
|
|
@@ -21,6 +22,7 @@ function make(name, options) {
|
|
|
21
22
|
input: options.input ?? NoInput,
|
|
22
23
|
success: options.success,
|
|
23
24
|
errors: options.errors ?? [],
|
|
25
|
+
access,
|
|
24
26
|
http: options.http !== false,
|
|
25
27
|
mcp
|
|
26
28
|
};
|
package/dist/ActionCli.d.mts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { Action_d_exports, Any, Handler } from "./Action.mjs";
|
|
2
2
|
import { Actions } from "./internal/actions.mjs";
|
|
3
3
|
import { HandlerContext, Handlers, HandlersContext, Implementation } from "./internal/implementation.mjs";
|
|
4
|
-
import { Options } from "./internal/cli.mjs";
|
|
5
|
-
import { Scope } from "effect";
|
|
4
|
+
import { Options as Options$1 } from "./internal/cli.mjs";
|
|
5
|
+
import { Effect, Scope } from "effect";
|
|
6
6
|
import { Command } from "effect/unstable/cli";
|
|
7
7
|
//#region src/ActionCli.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* The hook a local command runs before the selected handler. A CLI encodes
|
|
10
|
+
* nothing, so its refusal is a typed failure of the command effect rather than a
|
|
11
|
+
* declared surface error; the native parser has already decoded the input.
|
|
12
|
+
*/
|
|
13
|
+
interface BeforeOptions<E, R> {
|
|
14
|
+
readonly before?: (action: Any) => Effect.Effect<void, E, R>;
|
|
15
|
+
}
|
|
16
|
+
/** Parsing, rendering and the pre-handler hook of one local command. */
|
|
17
|
+
export type Options<Output, E = never, R = never, Parameters extends Command.Command.Config = never> = Options$1<Output, Parameters> & BeforeOptions<E, R>;
|
|
8
18
|
/** Configuration for an aggregate group command. */
|
|
9
|
-
export interface GroupOptions {
|
|
19
|
+
export interface GroupOptions<E = never, R = never> extends BeforeOptions<E, R> {
|
|
10
20
|
/** Override the group command name. */
|
|
11
21
|
readonly name?: string;
|
|
12
22
|
}
|
|
@@ -18,10 +28,7 @@ type BoundHandler<Name extends string, A extends Any, R> = Readonly<Record<Name,
|
|
|
18
28
|
* Project one explicitly selected local action into a native Effect CLI command.
|
|
19
29
|
* The action name resolves against the bound group; no HTTP fallback exists.
|
|
20
30
|
*/
|
|
21
|
-
export declare const command: <G extends Actions, Name extends G["actions"][number]["name"], H extends BoundHandler<Name, Selected<G, Name>, HandlerContext<H, Name>>, EX, RX, Parameters extends Command.Command.Config = never>(app: Implementation<G, H, EX, RX>, name: Name, options?: Options<Selected<G, Name>["success"]["Type"], Parameters>) => Command.Command<string, never, {},
|
|
22
|
-
readonly name: Name;
|
|
23
|
-
}>["errors"][number]["Type"], Exclude<RX, Scope.Scope> | Exclude<HandlerContext<H, Name>, Scope.Scope>>;
|
|
31
|
+
export declare const command: <G extends Actions, Name extends G["actions"][number]["name"], H extends BoundHandler<Name, Selected<G, Name>, HandlerContext<H, Name>>, EX, RX, EB = never, RB = never, Parameters extends Command.Command.Config = never>(app: Implementation<G, H, EX, RX>, name: Name, options?: Options<Selected<G, Name>["success"]["Type"], EB, RB, Parameters>) => Command.Command<string, never, {}, unknown, Exclude<RB, Scope.Scope> | Exclude<RX, Scope.Scope> | Exclude<HandlerContext<H, Name>, Scope.Scope>>;
|
|
24
32
|
/** Project every local action below its group namespace with default CLI options. */
|
|
25
|
-
export declare const group: <G extends Actions, H extends Handlers<HandlersContext<H>>, EX, RX>(app: Implementation<G, H, EX, RX>, options?: GroupOptions) => Command.Command<string, {}, {}, EX | import("effect/Schema").SchemaError | G["actions"][number]["errors"][number]["Type"], Exclude<Exclude<RX, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<HandlersContext<H>, Scope.Scope>, Command.CommandContext<string>>>;
|
|
26
|
-
//#endregion
|
|
27
|
-
export type { Options };
|
|
33
|
+
export declare const group: <G extends Actions, H extends Handlers<HandlersContext<H>>, EX, RX, EB = never, RB = never>(app: Implementation<G, H, EX, RX>, options?: GroupOptions<EB, RB>) => Command.Command<string, {}, {}, EB | EX | import("effect/Schema").SchemaError | G["actions"][number]["errors"][number]["Type"], Exclude<Exclude<RB, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<RX, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<HandlersContext<H>, Scope.Scope>, Command.CommandContext<string>>>;
|
|
34
|
+
//#endregion
|
package/dist/ActionCli.mjs
CHANGED
|
@@ -8,18 +8,19 @@ const select = (group, name) => {
|
|
|
8
8
|
if (action === void 0) throw new Error(`Unknown action "${group.name}.${name}"`);
|
|
9
9
|
return action;
|
|
10
10
|
};
|
|
11
|
-
const local = (app, action, input) => Effect.scoped(Effect.flatMap(app.build, (handlers) => dispatch(app.group, action, handlers)(input)));
|
|
11
|
+
const local = (app, action, before, input) => Effect.scoped(Effect.flatMap(app.build, (handlers) => dispatch(app.group, action, handlers, before)(input)));
|
|
12
12
|
/**
|
|
13
13
|
* Project one explicitly selected local action into a native Effect CLI command.
|
|
14
14
|
* The action name resolves against the bound group; no HTTP fallback exists.
|
|
15
15
|
*/
|
|
16
16
|
const command = (app, name, options) => {
|
|
17
17
|
const action = select(app.group, name);
|
|
18
|
-
return command$1(action, (input) => local(app, action, input), options);
|
|
18
|
+
return command$1(action, (input) => local(app, action, options?.before, input), options);
|
|
19
19
|
};
|
|
20
20
|
/** Project every local action below its group namespace with default CLI options. */
|
|
21
21
|
const group = (app, options) => {
|
|
22
|
-
const
|
|
22
|
+
const before = options?.before;
|
|
23
|
+
const commands = app.group.actions.map((action) => command$1(action, (input) => Effect.scoped(Effect.flatMap(app.build, (handlers) => dispatch(app.group, action, handlers, before)(input)))));
|
|
23
24
|
return Command.make(options?.name ?? app.group.name).pipe(Command.withSubcommands(commands));
|
|
24
25
|
};
|
|
25
26
|
//#endregion
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { Action_d_exports, Codec } from "./Action.mjs";
|
|
1
2
|
import { Actions } from "./internal/actions.mjs";
|
|
2
3
|
import { Options as Options$1 } from "./internal/cli.mjs";
|
|
3
4
|
import { ActionHttp_d_exports, Api, Http } from "./ActionHttp.mjs";
|
|
4
5
|
import { Effect } from "effect";
|
|
6
|
+
import { HttpClient } from "effect/unstable/http";
|
|
5
7
|
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
|
6
8
|
import { Command } from "effect/unstable/cli";
|
|
7
|
-
import { HttpClient } from "effect/unstable/http";
|
|
8
9
|
//#region src/ActionCliClient.d.ts
|
|
9
10
|
/** Parsing, rendering and native HTTP-endpoint configuration for one remote action. */
|
|
10
11
|
export type Options<Output, ParsedParameters extends Command.Command.Config = never> = Options$1<Output, ParsedParameters> & {
|
|
@@ -27,19 +28,19 @@ type Selected<G extends Actions, Name extends G["actions"][number]["name"]> = Ex
|
|
|
27
28
|
type HttpNames<G extends Actions> = Extract<G["actions"][number], {
|
|
28
29
|
readonly http: true;
|
|
29
30
|
}>["name"];
|
|
30
|
-
type ApiGroups<Groups extends ReadonlyArray<Actions>> = Api<Groups[number]> extends HttpApi.HttpApi<"actions", infer ApiGroups> ? ApiGroups : never;
|
|
31
|
-
type Endpoint<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = Extract<HttpApiGroup.EndpointsWithIdentifier<ApiGroups<Groups>, Extract<GroupName, HttpApiGroup.Identifier<ApiGroups<Groups>>>>, {
|
|
31
|
+
type ApiGroups<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>> = Api<Groups[number], Errors[number]> extends HttpApi.HttpApi<"actions", infer ApiGroups> ? ApiGroups : never;
|
|
32
|
+
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>>>>, {
|
|
32
33
|
readonly identifier: ActionName;
|
|
33
34
|
}>;
|
|
34
|
-
type NativeMethod<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = HttpApiClient.Client.Method<Extract<Endpoint<Groups, GroupName, ActionName>, HttpApiEndpoint.ConstraintRequest>, never, never>;
|
|
35
|
-
type RemoteCommand<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"], ActionName extends HttpNames<Group<Groups, GroupName>>> = Command.Command<string, never, {}, Effect.Error<ReturnType<NativeMethod<Groups, GroupName, ActionName>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, GroupName, ActionName>>>>;
|
|
36
|
-
type RemoteGroupCommand<Groups extends ReadonlyArray<Actions>, GroupName extends Groups[number]["name"]> = Command.Command<string, {}, {}, Effect.Error<ReturnType<NativeMethod<Groups, GroupName, HttpNames<Group<Groups, GroupName>>>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, GroupName, HttpNames<Group<Groups, GroupName>>>>>>;
|
|
35
|
+
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>;
|
|
36
|
+
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>>>>;
|
|
37
|
+
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
38
|
/**
|
|
38
39
|
* Project one HTTP action retained by the binding into a native Effect CLI command.
|
|
39
40
|
* Group and action selectors resolve exclusively from `http.groups`; runtime guards
|
|
40
41
|
* keep dynamically supplied selector strings from reaching the native client.
|
|
41
42
|
*/
|
|
42
|
-
export declare const command: <const Groups extends ReadonlyArray<Actions>, const GroupName extends Groups[number]["name"], const ActionName extends HttpNames<Group<Groups, GroupName>>, ParsedParameters extends Command.Command.Config = never>(http: Http<Groups>, groupName: GroupName, actionName: ActionName, options?: Options<Selected<Group<Groups, GroupName>, ActionName>["success"]["Type"], ParsedParameters>) => RemoteCommand<Groups, GroupName, ActionName>;
|
|
43
|
+
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>;
|
|
43
44
|
/** Project all HTTP-enabled actions retained by one group below its group namespace. */
|
|
44
|
-
export declare const group: <const Groups extends ReadonlyArray<Actions>, const GroupName extends Groups[number]["name"]>(http: Http<Groups>, groupName: GroupName, options?: GroupOptions) => RemoteGroupCommand<Groups, GroupName>;
|
|
45
|
+
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>;
|
|
45
46
|
//#endregion
|
package/dist/ActionCliClient.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { command as command$1 } from "./internal/cli.mjs";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
3
4
|
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
4
5
|
import { Command } from "effect/unstable/cli";
|
|
5
|
-
import { HttpClient } from "effect/unstable/http";
|
|
6
6
|
//#region src/ActionCliClient.ts
|
|
7
7
|
/**
|
|
8
8
|
* The native endpoint builder needs string selectors for a runtime-selected route.
|
package/dist/ActionGroup.d.mts
CHANGED
|
@@ -3,11 +3,11 @@ import { Actions, SchemaErrorPolicy } from "./internal/actions.mjs";
|
|
|
3
3
|
import { Implementation } from "./internal/implementation.mjs";
|
|
4
4
|
import { Effect, Scope } from "effect";
|
|
5
5
|
declare namespace ActionGroup_d_exports {
|
|
6
|
-
export { Any, Group, Implementation, Options, SchemaErrorPolicy, make };
|
|
6
|
+
export { Any, Contracts, Group, Implementation, Options, SchemaErrorPolicy, contracts, make };
|
|
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 Http, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], 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 Http, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Access, Http, Mcp> : never; };
|
|
11
11
|
/** What `make` needs to define a group. */
|
|
12
12
|
export interface Options<Name extends string, Errors extends ReadonlyArray<Codec>, PolicyErrors extends ReadonlyArray<Codec>> {
|
|
13
13
|
/** The `HttpApiGroup` identifier, and so the OpenAPI tag and operation-ID prefix. */
|
|
@@ -22,9 +22,21 @@ export interface Group<Name extends string, Actions$1 extends ReadonlyArray<Any$
|
|
|
22
22
|
/** Bind every handler at once, resolving build-time services once per adapter layer. */
|
|
23
23
|
readonly implement: <H extends HandlersFrom<Actions$1>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Group<Name, Actions$1, PolicyErrors>, H, NoInfer<EX>, NoInfer<Exclude<RX, Scope.Scope>>>;
|
|
24
24
|
}
|
|
25
|
-
/** Any group, with its actions erased. */
|
|
26
|
-
export type Any = Group<string, ReadonlyArray<Any$1>>;
|
|
25
|
+
/** Any group, with its actions and error schemas erased. */
|
|
26
|
+
export type Any = Group<string, ReadonlyArray<Any$1>, ReadonlyArray<Codec>>;
|
|
27
27
|
/** Define a group. Invalid or duplicate action and MCP names fail here, at definition time. */
|
|
28
28
|
export declare function make<const Name extends string, const Actions extends ReadonlyArray<Any$1>, const Errors extends ReadonlyArray<Codec> = [], const PolicyErrors extends ReadonlyArray<Codec> = []>(options: Options<Name, Errors, PolicyErrors>, ...actions: Actions): Group<Name, WithErrors<Actions, Errors>, PolicyErrors>;
|
|
29
|
+
/** `G extends unknown` distributes, so each group maps only its own actions. */
|
|
30
|
+
type PerGroup<G extends Actions> = G extends unknown ? { readonly [A in G["actions"][number] as `${G["name"]}.${A["name"]}`]: A; } : never;
|
|
31
|
+
/** Merges those per-group maps into one, rather than leaving a union of them. */
|
|
32
|
+
type Merged<Maps> = (Maps extends unknown ? (map: Maps) => void : never) extends ((map: infer Merged) => void) ? Merged : never;
|
|
33
|
+
/** Every action of `Groups`, keyed `<group>.<action>`. */
|
|
34
|
+
export type Contracts<Groups extends ReadonlyArray<Actions>> = Merged<PerGroup<Groups[number]>>;
|
|
35
|
+
/**
|
|
36
|
+
* Project groups into one map of their action contracts, keyed `<group>.<action>`:
|
|
37
|
+
* the identity routes, tools and commands share. Deriving it from the groups
|
|
38
|
+
* themselves means an added action cannot be missed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function contracts<const Groups extends ReadonlyArray<Actions>>(...groups: Groups): Contracts<Groups>;
|
|
29
41
|
//#endregion
|
|
30
42
|
export { ActionGroup_d_exports, type Implementation, type SchemaErrorPolicy };
|
package/dist/ActionGroup.mjs
CHANGED
|
@@ -22,5 +22,9 @@ function make(options, ...declared) {
|
|
|
22
22
|
};
|
|
23
23
|
return group;
|
|
24
24
|
}
|
|
25
|
+
function contracts(...groups) {
|
|
26
|
+
assertDistinct("contract group", groups.map((group) => group.name));
|
|
27
|
+
return Object.fromEntries(groups.flatMap((group) => group.actions.map((action) => [`${group.name}.${action.name}`, action])));
|
|
28
|
+
}
|
|
25
29
|
//#endregion
|
|
26
|
-
export { make };
|
|
30
|
+
export { contracts, make };
|
package/dist/ActionHttp.d.mts
CHANGED
|
@@ -1,39 +1,59 @@
|
|
|
1
1
|
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
|
-
import { Layer, Schema } from "effect";
|
|
5
|
-
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
|
4
|
+
import { Effect, Layer, Schema } from "effect";
|
|
6
5
|
import { Etag, HttpPlatform, HttpRouter } from "effect/unstable/http";
|
|
6
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
|
7
7
|
import { FileSystem } from "effect/FileSystem";
|
|
8
8
|
import { Path } from "effect/Path";
|
|
9
9
|
declare namespace ActionHttp_d_exports {
|
|
10
|
-
export { Api, Http, Options, make };
|
|
10
|
+
export { Api, Http, LayerOptions, Options, make };
|
|
11
11
|
}
|
|
12
12
|
/** Contract-level configuration: servers and clients must agree on it. */
|
|
13
|
-
export interface Options {
|
|
13
|
+
export interface Options<Errors extends ReadonlyArray<Codec> = []> {
|
|
14
14
|
readonly apiPath: `/${string}`;
|
|
15
|
+
/**
|
|
16
|
+
* Failures the surface around these endpoints answers with instead of a
|
|
17
|
+
* handler: authentication, authorization, rate limits. Declared on every
|
|
18
|
+
* endpoint, so `HttpApiClient` and `ActionCliClient` decode them as typed
|
|
19
|
+
* failures rather than reporting a decode error. Each schema keeps its own
|
|
20
|
+
* `httpApiStatus`, and their `_tag`s must be distinct.
|
|
21
|
+
*/
|
|
22
|
+
readonly errors?: Errors;
|
|
23
|
+
}
|
|
24
|
+
/** What `Http.layer` binds around the implementations it serves. */
|
|
25
|
+
export interface LayerOptions<Errors extends ReadonlyArray<Codec>, R> {
|
|
26
|
+
/**
|
|
27
|
+
* Runs once per request, before the request payload is decoded, with the action
|
|
28
|
+
* contract it is about to run. It fails with the binding's own `errors`, so an
|
|
29
|
+
* unauthorized caller learns nothing about the action's input, and the refusal
|
|
30
|
+
* is encoded exactly like a declared error. Its services are request-time
|
|
31
|
+
* requirements, like a handler's.
|
|
32
|
+
*/
|
|
33
|
+
readonly before?: (action: Any) => Effect.Effect<void, Errors[number]["Type"], R>;
|
|
15
34
|
}
|
|
16
35
|
type Endpoint<A extends Any, E extends Codec> = A extends {
|
|
17
36
|
readonly http: false;
|
|
18
37
|
} ? never : A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["errors"][number] | E>, never> : never;
|
|
19
|
-
type ApiGroup<G extends Actions> = G extends Actions ? [Endpoint<G["actions"][number], never>] extends [never] ? never : HttpApiGroup.HttpApiGroup<G["name"], Endpoint<G["actions"][number], PolicyError<G
|
|
38
|
+
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;
|
|
20
39
|
/** The native `HttpApi` of every HTTP-enabled action of `G`, one group per action group. */
|
|
21
|
-
export type Api<G extends Actions> = HttpApi.HttpApi<"actions", ApiGroup<G>>;
|
|
40
|
+
export type Api<G extends Actions, E extends Codec = never> = HttpApi.HttpApi<"actions", ApiGroup<G, E>>;
|
|
22
41
|
/** A namespaced HTTP binding. */
|
|
23
|
-
export interface Http<Groups extends ReadonlyArray<Actions
|
|
42
|
+
export interface Http<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec> = []> {
|
|
24
43
|
/** The exact contracts bound to this HTTP adapter, in declaration order. */
|
|
25
44
|
readonly groups: Groups;
|
|
26
|
-
readonly api: Api<Groups[number]>;
|
|
45
|
+
readonly api: Api<Groups[number], Errors[number]>;
|
|
27
46
|
/**
|
|
28
|
-
* Serve any number of implementations in one layer
|
|
29
|
-
*
|
|
47
|
+
* Serve any number of implementations in one layer, with one pre-handler hook
|
|
48
|
+
* around every request they answer. Build failures, build services and request
|
|
49
|
+
* services are unions over precisely those apps and that hook.
|
|
30
50
|
*/
|
|
31
|
-
readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]
|
|
51
|
+
readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>, RB = never>(options: LayerOptions<Errors, RB>, ...apps: Apps) => 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>;
|
|
32
52
|
}
|
|
33
53
|
/**
|
|
34
54
|
* Bind the contract-level configuration once. Every route is `POST
|
|
35
55
|
* <apiPath>/<group>/<action>`, so action names may repeat across groups.
|
|
36
56
|
*/
|
|
37
|
-
export declare function make<const G extends ReadonlyArray<Actions
|
|
57
|
+
export declare function make<const G extends ReadonlyArray<Actions>, const E extends ReadonlyArray<Codec> = []>(options: Options<E>, ...groups: G): Http<G, E>;
|
|
38
58
|
//#endregion
|
|
39
59
|
export { ActionHttp_d_exports };
|
package/dist/ActionHttp.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { assertDistinct } from "./internal/actions.mjs";
|
|
1
|
+
import { assertDistinct, projectedErrors } from "./internal/actions.mjs";
|
|
2
2
|
import { dispatch } from "./internal/implementation.mjs";
|
|
3
3
|
import { Effect, Layer } from "effect";
|
|
4
|
+
import { HttpEffect, HttpRouter, HttpServerResponse } from "effect/unstable/http";
|
|
4
5
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
|
|
5
6
|
//#region src/ActionHttp.ts
|
|
6
7
|
const httpActions = (group) => group.actions.filter((action) => action.http);
|
|
7
|
-
const endpoint = (
|
|
8
|
+
const endpoint = (options, group, action) => HttpApiEndpoint.post(action.name, `${options.apiPath}/${group.name}/${action.name}`, {
|
|
8
9
|
payload: action.input,
|
|
9
10
|
success: action.success,
|
|
10
|
-
error: action.errors
|
|
11
|
+
error: projectedErrors(action, options.errors)
|
|
11
12
|
}).annotate(OpenApi.Description, action.description);
|
|
12
13
|
/** The native middleware declares the policy's errors on every endpoint it wraps. */
|
|
13
14
|
const schemaErrorMiddleware = (name, policy) => {
|
|
@@ -17,18 +18,36 @@ const schemaErrorMiddleware = (name, policy) => {
|
|
|
17
18
|
layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy.map(failure)))
|
|
18
19
|
};
|
|
19
20
|
};
|
|
21
|
+
/** The hook of one group, applied to every one of its endpoints. */
|
|
22
|
+
const hookMiddleware = (name) => {
|
|
23
|
+
class Admission extends HttpApiMiddleware.Service()(`effect-actions/http/Admission/${name}`) {}
|
|
24
|
+
const implementation = (group, before) => {
|
|
25
|
+
if (before === void 0) return (httpEffect) => httpEffect;
|
|
26
|
+
const actions = new Map(httpActions(group).map((action) => [action.name, action]));
|
|
27
|
+
return (httpEffect, { endpoint }) => {
|
|
28
|
+
const action = actions.get(endpoint.identifier);
|
|
29
|
+
if (action === void 0) throw new Error(`Missing action: ${endpoint.identifier}`);
|
|
30
|
+
return Effect.flatMap(before(action), () => httpEffect);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
Admission,
|
|
35
|
+
layer: (group, before) => Layer.succeed(Admission, implementation(group, before))
|
|
36
|
+
};
|
|
37
|
+
};
|
|
20
38
|
function bind(options, group) {
|
|
21
|
-
const [first, ...rest] = httpActions(group).map((action) => endpoint(options
|
|
39
|
+
const [first, ...rest] = httpActions(group).map((action) => endpoint(options, group, action));
|
|
22
40
|
if (first === void 0) return void 0;
|
|
23
|
-
const
|
|
41
|
+
const bound = HttpApiGroup.make(group.name).add(first, ...rest);
|
|
42
|
+
const hook = hookMiddleware(group.name);
|
|
24
43
|
if (group.schemaError === void 0) return {
|
|
25
|
-
native,
|
|
26
|
-
|
|
44
|
+
native: bound.middleware(hook.Admission),
|
|
45
|
+
middleware: (before) => hook.layer(group, before)
|
|
27
46
|
};
|
|
28
47
|
const policy = schemaErrorMiddleware(group.name, group.schemaError);
|
|
29
48
|
return {
|
|
30
|
-
native:
|
|
31
|
-
|
|
49
|
+
native: bound.middleware(policy.SchemaErrors).middleware(hook.Admission),
|
|
50
|
+
middleware: (before) => Layer.merge(policy.layer, hook.layer(group, before))
|
|
32
51
|
};
|
|
33
52
|
}
|
|
34
53
|
function apiOf(groups) {
|
|
@@ -47,17 +66,22 @@ const erasedHandlers = (handlers) => {
|
|
|
47
66
|
return handlers;
|
|
48
67
|
};
|
|
49
68
|
/** Register one group's handlers. Handler erasure is limited to native dynamic endpoint registration. */
|
|
50
|
-
const groupHandlers = (api, app,
|
|
51
|
-
const entries = (record) => Object.fromEntries(httpActions(app.group).map((action) => [action.name, (request) => dispatch(app.group, action, record)(request.payload)]));
|
|
52
|
-
return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.group(api, app.group.name, (builder) => builder.handleAll(entries(erasedHandlers(record)))))).pipe(Layer.provide(
|
|
69
|
+
const groupHandlers = (api, app, middleware) => {
|
|
70
|
+
const entries = (record) => Object.fromEntries(httpActions(app.group).map((action) => [action.name, (request) => dispatch(app.group, action, record, void 0)(request.payload)]));
|
|
71
|
+
return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.group(api, app.group.name, (builder) => builder.handleAll(entries(erasedHandlers(record)))))).pipe(Layer.provide(middleware));
|
|
53
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* A refused or failed request is never stored by a shared cache. Successful
|
|
75
|
+
* responses keep whatever the host sets, since only the host knows what is public.
|
|
76
|
+
*/
|
|
77
|
+
const noStore = HttpRouter.middleware((httpEffect) => HttpEffect.withPreResponseHandler(httpEffect, (_request, response) => Effect.succeed(response.status < 400 ? response : HttpServerResponse.setHeader(response, "cache-control", "no-store"))));
|
|
54
78
|
function make(options, ...groups) {
|
|
55
79
|
assertDistinct("action group", groups.map((group) => group.name));
|
|
56
80
|
const bound = new Map(groups.flatMap((group) => {
|
|
57
81
|
const binding = bind(options, group);
|
|
58
82
|
return binding === void 0 ? [] : [[group, binding]];
|
|
59
83
|
}));
|
|
60
|
-
const layer = (...apps) => {
|
|
84
|
+
const layer = (layerOptions, ...apps) => {
|
|
61
85
|
assertDistinct("implementation group", apps.map((app) => app.group.name));
|
|
62
86
|
for (const app of apps) if (!groups.includes(app.group)) throw new Error(`Implementation of group "${app.group.name}" is not served by this adapter`);
|
|
63
87
|
const served = apps.flatMap((app) => {
|
|
@@ -68,7 +92,8 @@ function make(options, ...groups) {
|
|
|
68
92
|
}];
|
|
69
93
|
});
|
|
70
94
|
const api = apiOf(served.map(({ native }) => native));
|
|
71
|
-
|
|
95
|
+
const before = layerOptions.before;
|
|
96
|
+
return served.reduce((layer, { app, middleware }) => layer.pipe(Layer.provide(groupHandlers(api, app, middleware(before)))), HttpApiBuilder.layer(api)).pipe(Layer.provide(noStore.layer));
|
|
72
97
|
};
|
|
73
98
|
return {
|
|
74
99
|
groups,
|
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
+
import { Action_d_exports, Any, Codec } from "./Action.mjs";
|
|
1
2
|
import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
|
|
2
|
-
import { Cause, Layer } from "effect";
|
|
3
|
-
import { McpSchema, McpServer } from "effect/unstable/ai";
|
|
3
|
+
import { Cause, Effect, Layer } from "effect";
|
|
4
4
|
import { HttpRouter } from "effect/unstable/http";
|
|
5
|
+
import { McpSchema, McpServer } from "effect/unstable/ai";
|
|
5
6
|
import { Stdio } from "effect/Stdio";
|
|
6
7
|
//#region src/ActionMcp.d.ts
|
|
8
|
+
/** What every MCP transport binds around the tools it serves. */
|
|
9
|
+
interface SurfaceOptions<Errors extends ReadonlyArray<Codec>, R> {
|
|
10
|
+
/**
|
|
11
|
+
* Failures the surface answers with instead of a handler: authorization, rate
|
|
12
|
+
* limits. Declared on every tool of this transport, so a refusal is returned
|
|
13
|
+
* as an `isError` result exactly like an action's own error.
|
|
14
|
+
*/
|
|
15
|
+
readonly errors?: Errors;
|
|
16
|
+
/**
|
|
17
|
+
* Runs once per tool call, after the native server has decoded the arguments
|
|
18
|
+
* and before the selected handler, with the action contract it is about to run.
|
|
19
|
+
* It fails with this transport's `errors`. Its services are request-time
|
|
20
|
+
* requirements, like a handler's.
|
|
21
|
+
*/
|
|
22
|
+
readonly before?: (action: Any) => Effect.Effect<void, Errors[number]["Type"], R>;
|
|
23
|
+
}
|
|
7
24
|
/** One Streamable HTTP MCP endpoint; its fields become the native server info. */
|
|
8
|
-
export interface Options {
|
|
25
|
+
export interface Options<Errors extends ReadonlyArray<Codec> = [], R = never> extends SurfaceOptions<Errors, R> {
|
|
9
26
|
readonly name: string;
|
|
10
27
|
readonly version: string;
|
|
11
28
|
/** Native protocol adapters to serve; negotiation and sessions are owned by Effect. */
|
|
@@ -17,7 +34,7 @@ export interface Options {
|
|
|
17
34
|
readonly instructions?: string;
|
|
18
35
|
}
|
|
19
36
|
/** Server information for an MCP subprocess connected through standard I/O. */
|
|
20
|
-
export interface StdioOptions {
|
|
37
|
+
export interface StdioOptions<Errors extends ReadonlyArray<Codec> = [], R = never> extends SurfaceOptions<Errors, R> {
|
|
21
38
|
readonly name: string;
|
|
22
39
|
readonly version: string;
|
|
23
40
|
/** Native protocol adapters to serve; negotiation is owned by Effect. */
|
|
@@ -25,19 +42,19 @@ export interface StdioOptions {
|
|
|
25
42
|
readonly instructions?: string;
|
|
26
43
|
}
|
|
27
44
|
/** The native server supplies its own request context to every tool call. */
|
|
28
|
-
type ToolRequestContext<App> = Exclude<RequestContext<App
|
|
45
|
+
type ToolRequestContext<App, RB> = Exclude<RequestContext<App> | RB, McpSchema.McpRequestContext>;
|
|
29
46
|
/**
|
|
30
47
|
* Serve MCP tools over one Streamable HTTP endpoint.
|
|
31
48
|
*
|
|
32
49
|
* Middleware provided around this layer has the normal HTTP lifetime. Native
|
|
33
50
|
* context capture applies: never provide request-identity tags at startup.
|
|
34
51
|
*/
|
|
35
|
-
export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation
|
|
52
|
+
export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(options: Options<Errors, RB>, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ToolRequestContext<Apps[number], RB>>>;
|
|
36
53
|
/**
|
|
37
54
|
* Serve MCP tools through newline-delimited JSON-RPC on standard I/O.
|
|
38
55
|
*
|
|
39
56
|
* The host supplies the `Stdio` service. Arguments are tool input only and
|
|
40
57
|
* never establish request identity or authority.
|
|
41
58
|
*/
|
|
42
|
-
export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation
|
|
59
|
+
export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(options: StdioOptions<Errors, RB>, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | Stdio | ToolRequestContext<Apps[number], RB>>;
|
|
43
60
|
//#endregion
|
package/dist/ActionMcp.mjs
CHANGED
|
@@ -3,13 +3,21 @@ import { bindTools } from "./internal/tools.mjs";
|
|
|
3
3
|
import { Layer } from "effect";
|
|
4
4
|
import { McpServer } from "effect/unstable/ai";
|
|
5
5
|
//#region src/ActionMcp.ts
|
|
6
|
-
const registration = (apps) => {
|
|
7
|
-
const binding = bindTools(apps, "mcp");
|
|
6
|
+
const registration = (apps, options) => {
|
|
7
|
+
const binding = bindTools(apps, "mcp", options);
|
|
8
8
|
return Layer.effectDiscard(McpServer.registerToolkit(binding.toolkit)).pipe(Layer.provide(binding.layer));
|
|
9
9
|
};
|
|
10
|
-
const server = (apps, transport) => registration(apps).pipe(Layer.provide(transport), Layer.fresh);
|
|
10
|
+
const server = (apps, options, transport) => registration(apps, options).pipe(Layer.provide(transport), Layer.fresh);
|
|
11
|
+
/** The hook and surface errors an erased transport binds. */
|
|
12
|
+
const toolOptions = (options) => {
|
|
13
|
+
const before = options.before;
|
|
14
|
+
return {
|
|
15
|
+
errors: options.errors,
|
|
16
|
+
before
|
|
17
|
+
};
|
|
18
|
+
};
|
|
11
19
|
function layerHttp(options, ...apps) {
|
|
12
|
-
return server(apps, McpServer.layerHttp({
|
|
20
|
+
return server(apps, toolOptions(options), McpServer.layerHttp({
|
|
13
21
|
name: options.name,
|
|
14
22
|
version: options.version,
|
|
15
23
|
instructions: options.instructions,
|
|
@@ -19,7 +27,7 @@ function layerHttp(options, ...apps) {
|
|
|
19
27
|
}));
|
|
20
28
|
}
|
|
21
29
|
function layerStdio(options, ...apps) {
|
|
22
|
-
return server(apps, McpServer.layerStdio(options));
|
|
30
|
+
return server(apps, toolOptions(options), McpServer.layerStdio(options));
|
|
23
31
|
}
|
|
24
32
|
//#endregion
|
|
25
33
|
export { layerHttp, layerStdio };
|