@gjermundgaraba/effect-actions 0.5.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/README.md +7 -6
- package/dist/Action.d.mts +16 -8
- package/dist/Action.mjs +0 -1
- package/dist/ActionCatalog.d.mts +2 -3
- package/dist/ActionCatalog.mjs +4 -5
- package/dist/ActionCli.mjs +5 -1
- package/dist/ActionCliClient.d.mts +8 -9
- package/dist/ActionCliClient.mjs +4 -11
- package/dist/ActionGroup.d.mts +15 -11
- package/dist/ActionGroup.mjs +8 -2
- package/dist/ActionHttp.d.mts +9 -5
- package/dist/ActionHttp.mjs +14 -9
- package/dist/ActionHttpClient.d.mts +33 -0
- package/dist/ActionHttpClient.mjs +16 -0
- package/dist/ActionMcp.d.mts +16 -26
- package/dist/ActionMcp.mjs +12 -8
- package/dist/ActionToolkit.d.mts +10 -11
- package/dist/Testing.d.mts +35 -4
- package/dist/Testing.mjs +53 -6
- package/dist/TestingClient.d.mts +6 -5
- package/dist/TestingClient.mjs +6 -3
- package/dist/internal/actions.d.mts +18 -7
- package/dist/internal/actions.mjs +11 -1
- 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 +6 -5
- package/dist/internal/tools.mjs +1 -1
- 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 +24 -16
- package/docs/ActionHttp.md +14 -12
- package/docs/ActionHttpClient.md +80 -0
- package/docs/ActionMcp.md +19 -22
- package/docs/ActionToolkit.md +1 -1
- package/docs/CONTEXT.md +4 -4
- package/docs/README.md +18 -18
- package/docs/Testing.md +27 -16
- package/docs/guarantees.md +5 -5
- package/docs/setup.md +7 -6
- package/package.json +5 -4
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;
|
|
@@ -33,4 +34,34 @@ export interface McpRequestParams {
|
|
|
33
34
|
}
|
|
34
35
|
/** Build one stateless 2026-07-28 JSON-RPC request, with client metadata defaulted. */
|
|
35
36
|
export declare const mcpRequest: ({ url, method, params, headers: init }: McpRequestOptions) => Request;
|
|
37
|
+
/** One `tools/call` for `mcpCall`: the endpoint, the tool and its arguments. */
|
|
38
|
+
export interface McpCallOptions {
|
|
39
|
+
readonly url: string | URL;
|
|
40
|
+
/** The tool name, `mcp.name` of its action. */
|
|
41
|
+
readonly name: string;
|
|
42
|
+
/** Defaults to `{}`. Like `params`, it may be malformed on purpose. */
|
|
43
|
+
readonly arguments?: {
|
|
44
|
+
readonly [key: string]: McpRequestValue;
|
|
45
|
+
};
|
|
46
|
+
readonly headers?: ConstructorParameters<typeof Headers>[0];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A tool call's outcome with the library's wire envelope removed: the success from
|
|
50
|
+
* `structuredContent.value`, or the error text of an `isError` result, parsed as JSON
|
|
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).
|
|
53
|
+
*/
|
|
54
|
+
export type McpCallResult = {
|
|
55
|
+
readonly isError: false;
|
|
56
|
+
readonly value: Schema.Json;
|
|
57
|
+
} | {
|
|
58
|
+
readonly isError: true;
|
|
59
|
+
readonly error: Schema.Json;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Call one tool through `handler` with a stateless 2026-07-28 request and return its
|
|
63
|
+
* outcome. A response that is not an HTTP 200 carrying a tool result, such as an
|
|
64
|
+
* authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
|
|
65
|
+
*/
|
|
66
|
+
export declare const mcpCall: (handler: Handler, { url, name, headers, arguments: args }: McpCallOptions) => Promise<McpCallResult>;
|
|
36
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);
|
|
@@ -38,5 +37,53 @@ const mcpRequest = ({ url, method, params = {}, headers: init }) => {
|
|
|
38
37
|
})
|
|
39
38
|
});
|
|
40
39
|
};
|
|
40
|
+
/** The JSON-RPC response to a `tools/call`: a tool result or a protocol error. */
|
|
41
|
+
const ToolReply = Schema.Union([Schema.Struct({ result: Schema.Struct({
|
|
42
|
+
isError: Schema.optionalKey(Schema.Boolean),
|
|
43
|
+
structuredContent: Schema.optionalKey(Schema.Struct({ value: Schema.Json })),
|
|
44
|
+
content: Schema.Array(Schema.Struct({
|
|
45
|
+
type: Schema.String,
|
|
46
|
+
text: Schema.optionalKey(Schema.String)
|
|
47
|
+
}))
|
|
48
|
+
}) }), Schema.Struct({ error: Schema.Struct({
|
|
49
|
+
code: Schema.Finite,
|
|
50
|
+
message: Schema.String
|
|
51
|
+
}) })]);
|
|
52
|
+
const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolReply));
|
|
53
|
+
const parseJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
|
|
54
|
+
/**
|
|
55
|
+
* Call one tool through `handler` with a stateless 2026-07-28 request and return its
|
|
56
|
+
* outcome. A response that is not an HTTP 200 carrying a tool result, such as an
|
|
57
|
+
* authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
|
|
58
|
+
*/
|
|
59
|
+
const mcpCall = async (handler, { url, name, headers, arguments: args = {} }) => {
|
|
60
|
+
const response = await handler(mcpRequest({
|
|
61
|
+
url,
|
|
62
|
+
method: "tools/call",
|
|
63
|
+
params: {
|
|
64
|
+
name,
|
|
65
|
+
arguments: args
|
|
66
|
+
},
|
|
67
|
+
...headers === void 0 ? {} : { headers }
|
|
68
|
+
}));
|
|
69
|
+
const body = await response.text();
|
|
70
|
+
if (response.status !== 200) throw new Error(`MCP tools/call "${name}" answered ${response.status}: ${body}`);
|
|
71
|
+
const reply = body.split("\n").map((line) => line.replace(/^data:/, "").trim()).flatMap((line) => Option.toArray(decodeReply(line))).at(-1);
|
|
72
|
+
if (reply === void 0) throw new Error(`MCP tools/call "${name}" had no reply: ${body}`);
|
|
73
|
+
if ("error" in reply) throw new Error(`MCP tools/call "${name}" failed with ${reply.error.code}: ${reply.error.message}`);
|
|
74
|
+
const { result } = reply;
|
|
75
|
+
if (result.isError === true) {
|
|
76
|
+
const text = result.content.find((content) => content.type === "text")?.text ?? "";
|
|
77
|
+
return {
|
|
78
|
+
isError: true,
|
|
79
|
+
error: Option.getOrElse(parseJson(text), () => text)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (result.structuredContent === void 0) throw new Error(`MCP tools/call "${name}" returned no structured content: ${body}`);
|
|
83
|
+
return {
|
|
84
|
+
isError: false,
|
|
85
|
+
value: result.structuredContent.value
|
|
86
|
+
};
|
|
87
|
+
};
|
|
41
88
|
//#endregion
|
|
42
|
-
export { httpClient, mcpRequest };
|
|
89
|
+
export { httpClient, mcpCall, mcpRequest };
|
package/dist/TestingClient.d.mts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { Client
|
|
1
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
2
2
|
//#region src/TestingClient.d.ts
|
|
3
3
|
/** How `withMcpClient` connects the official client. */
|
|
4
4
|
export interface McpClientOptions {
|
|
5
5
|
/** A web handler, such as `HttpRouter.toWebHandler(routes).handler`. */
|
|
6
6
|
readonly fetch: (request: Request) => Promise<Response>;
|
|
7
7
|
readonly path: string;
|
|
8
|
-
/** Passed directly to the official client; omitted options use its native default. */
|
|
9
|
-
readonly versionNegotiation?: ClientOptions["versionNegotiation"];
|
|
10
8
|
/** Defaults to `http://localhost`. */
|
|
11
9
|
readonly baseUrl?: string | URL;
|
|
12
10
|
readonly headers?: ConstructorParameters<typeof Headers>[0];
|
|
13
11
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Connect an official client pinned to MCP 2026-07-28, the revision `ActionMcp`
|
|
14
|
+
* serves, and always close its transport after the callback.
|
|
15
|
+
*/
|
|
16
|
+
export declare const withMcpClient: <A>({ fetch, path, baseUrl, headers }: McpClientOptions, run: (client: Client) => Promise<A>) => Promise<A>;
|
|
16
17
|
//#endregion
|
package/dist/TestingClient.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
2
2
|
//#region src/TestingClient.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Connect an official client pinned to MCP 2026-07-28, the revision `ActionMcp`
|
|
5
|
+
* serves, and always close its transport after the callback.
|
|
6
|
+
*/
|
|
7
|
+
const withMcpClient = async ({ fetch, path, baseUrl = "http://localhost", headers }, run) => {
|
|
5
8
|
const client = new Client({
|
|
6
9
|
name: "test",
|
|
7
10
|
version: "0"
|
|
8
|
-
}, versionNegotiation
|
|
11
|
+
}, { versionNegotiation: { mode: { pin: "2026-07-28" } } });
|
|
9
12
|
try {
|
|
10
13
|
const transportOptions = { fetch: (input, init) => fetch(new Request(input, init)) };
|
|
11
14
|
if (headers !== void 0) transportOptions.requestInit = { headers };
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { Action_d_exports, Any, Codec } from "../Action.mjs";
|
|
2
2
|
import { HttpApiError } from "effect/unstable/httpapi";
|
|
3
3
|
//#region src/internal/actions.d.ts
|
|
4
|
-
/**
|
|
5
|
-
export interface
|
|
6
|
-
|
|
7
|
-
readonly
|
|
4
|
+
/** One side of a schema-error policy: the error it answers with, and how to make it. */
|
|
5
|
+
export interface SchemaErrorAnswer<E extends Codec> {
|
|
6
|
+
/** A declared error schema; its `httpApiStatus` is the HTTP status of the answer. */
|
|
7
|
+
readonly schema: E;
|
|
8
|
+
readonly make: (failure: HttpApiError.HttpApiSchemaError) => NoInfer<E["Type"]>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pure HTTP policy for native schema failures, split by whose fault they are. The
|
|
12
|
+
* library owns the split, so every group draws it the same way.
|
|
13
|
+
*/
|
|
14
|
+
export interface SchemaErrorPolicy<Invalid extends Codec = Codec, Internal extends Codec = Codec> {
|
|
15
|
+
/** The request did not decode: its payload, or its params, headers or query. */
|
|
16
|
+
readonly invalid: SchemaErrorAnswer<Invalid>;
|
|
17
|
+
/** The handler's result did not encode: its body or its response headers. */
|
|
18
|
+
readonly internal: SchemaErrorAnswer<Internal>;
|
|
8
19
|
}
|
|
9
20
|
/** The contract half of a group: what adapters and clients need, without `implement`. */
|
|
10
|
-
export interface Actions<Name extends string = string, A extends ReadonlyArray<Any> = ReadonlyArray<Any>,
|
|
21
|
+
export interface Actions<Name extends string = string, A extends ReadonlyArray<Any> = ReadonlyArray<Any>, PolicyError extends Codec = Codec> {
|
|
11
22
|
readonly name: Name;
|
|
12
23
|
readonly actions: A;
|
|
13
24
|
/** How HTTP answers failed decoding or encoding; native behavior without one. MCP is unaffected. */
|
|
14
|
-
readonly schemaError: SchemaErrorPolicy<
|
|
25
|
+
readonly schemaError: SchemaErrorPolicy<PolicyError, PolicyError> | undefined;
|
|
15
26
|
}
|
|
16
27
|
/** The errors a group's policy may answer with, by lookup rather than a conditional type. */
|
|
17
|
-
export type PolicyError<G extends Actions> = NonNullable<G["schemaError"]>["
|
|
28
|
+
export type PolicyError<G extends Actions> = NonNullable<G["schemaError"]>[keyof SchemaErrorPolicy]["schema"];
|
|
18
29
|
//#endregion
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
//#region src/internal/actions.ts
|
|
2
|
+
/** Each distinct error a policy may answer with, invalid first. */
|
|
3
|
+
const policyErrors = (policy) => [.../* @__PURE__ */ new Set([policy.invalid.schema, policy.internal.schema])];
|
|
4
|
+
/**
|
|
5
|
+
* `HttpApiBuilder` reports these kinds while encoding the handler's answer, after the
|
|
6
|
+
* handler ran; every other kind (`Payload`, `Params`, `Headers`, `Query`) comes from
|
|
7
|
+
* decoding the request before it.
|
|
8
|
+
*/
|
|
9
|
+
const responseKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
|
|
10
|
+
/** The policy's answer to one native failure: `internal` for the response side, else `invalid`. */
|
|
11
|
+
const answerSchemaError = (policy, failure) => responseKinds.has(failure.kind) ? policy.internal.make(failure) : policy.invalid.make(failure);
|
|
2
12
|
/**
|
|
3
13
|
* An action's own failures plus the ones its surface answers with, which is what
|
|
4
14
|
* a projection of that action declares. A schema the action already declares is
|
|
@@ -28,4 +38,4 @@ const assertDistinct = (what, names) => {
|
|
|
28
38
|
}
|
|
29
39
|
};
|
|
30
40
|
//#endregion
|
|
31
|
-
export { assertDistinct, assertName, projectedErrors, selectNamed };
|
|
41
|
+
export { answerSchemaError, assertDistinct, assertName, policyErrors, projectedErrors, selectNamed };
|
|
@@ -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.
|
|
@@ -22,15 +22,16 @@ var Implementation = class Implementation {
|
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
/**
|
|
25
|
-
* Select
|
|
26
|
-
*
|
|
25
|
+
* Select the handler of a concrete action, once, when an adapter binds its acquired
|
|
26
|
+
* record; `implement` has already refused a record without it. The returned function
|
|
27
|
+
* invokes it: the surface's pre-handler hook runs first, outside the action's span, so a
|
|
27
28
|
* refusal is attributed to the surface rather than to a handler that never ran.
|
|
28
29
|
* `R` remains in the returned effect so transport layers cannot erase required
|
|
29
30
|
* per-request services while assembling routes.
|
|
30
31
|
*/
|
|
31
32
|
const dispatch = (group, action, handlers, before) => {
|
|
32
|
-
const handle = handlers[action.name];
|
|
33
|
-
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}`);
|
|
34
35
|
const attributes = {
|
|
35
36
|
"action.group": group.name,
|
|
36
37
|
"action.name": action.name,
|
package/dist/internal/tools.mjs
CHANGED
|
@@ -47,7 +47,7 @@ const project = (projection, entry, options) => {
|
|
|
47
47
|
return tool.annotate(Tool.Strict, true);
|
|
48
48
|
};
|
|
49
49
|
const handler = (projection, app, action, handlers, before) => {
|
|
50
|
-
const run =
|
|
50
|
+
const run = dispatch(app.group, action, handlers, before);
|
|
51
51
|
return projection === "native" ? run : (input) => Effect.map(run(input), (value) => ({ value }));
|
|
52
52
|
};
|
|
53
53
|
/**
|
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.
|