@gjermundgaraba/effect-actions 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +16 -13
  2. package/dist/ActionCatalog.d.mts +2 -2
  3. package/dist/ActionCatalog.mjs +11 -10
  4. package/dist/ActionCli.d.mts +2 -2
  5. package/dist/ActionCli.mjs +7 -8
  6. package/dist/ActionCliClient.d.mts +9 -6
  7. package/dist/ActionCliClient.mjs +9 -11
  8. package/dist/ActionGroup.d.mts +17 -15
  9. package/dist/ActionGroup.mjs +12 -1
  10. package/dist/ActionHttp.d.mts +10 -5
  11. package/dist/ActionHttp.mjs +21 -38
  12. package/dist/ActionHttpClient.d.mts +45 -0
  13. package/dist/ActionHttpClient.mjs +24 -0
  14. package/dist/ActionMcp.d.mts +17 -43
  15. package/dist/ActionMcp.mjs +15 -19
  16. package/dist/ActionToolkit.d.mts +5 -17
  17. package/dist/ActionToolkit.mjs +2 -7
  18. package/dist/Testing.d.mts +31 -1
  19. package/dist/Testing.mjs +50 -2
  20. package/dist/TestingClient.d.mts +6 -5
  21. package/dist/TestingClient.mjs +6 -3
  22. package/dist/internal/actions.d.mts +18 -7
  23. package/dist/internal/actions.mjs +21 -2
  24. package/dist/internal/cli.d.mts +2 -2
  25. package/dist/internal/cli.mjs +34 -20
  26. package/dist/internal/implementation.d.mts +17 -6
  27. package/dist/internal/implementation.mjs +12 -3
  28. package/dist/internal/tools.d.mts +22 -0
  29. package/dist/internal/tools.mjs +3 -18
  30. package/docs/Action.md +22 -54
  31. package/docs/ActionCatalog.md +32 -35
  32. package/docs/ActionCli.md +25 -52
  33. package/docs/ActionCliClient.md +16 -30
  34. package/docs/ActionGroup.md +34 -56
  35. package/docs/ActionHttp.md +34 -46
  36. package/docs/ActionHttpClient.md +73 -0
  37. package/docs/ActionMcp.md +117 -89
  38. package/docs/ActionToolkit.md +28 -46
  39. package/docs/Authentication.md +18 -36
  40. package/docs/CONTEXT.md +6 -5
  41. package/docs/README.md +30 -24
  42. package/docs/Testing.md +62 -59
  43. package/docs/guarantees.md +13 -13
  44. package/docs/setup.md +7 -6
  45. package/package.json +5 -4
@@ -1,60 +1,34 @@
1
- import { Action_d_exports, Any, Codec } from "./Action.mjs";
1
+ import { Action_d_exports, Codec } from "./Action.mjs";
2
2
  import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
3
- import { Cause, Effect, Layer } from "effect";
3
+ import { SurfaceOptions } from "./internal/tools.mjs";
4
+ import { Cause, Layer } from "effect";
4
5
  import { HttpRouter } from "effect/unstable/http";
5
6
  import { McpSchema, McpServer } from "effect/unstable/ai";
6
7
  import { Stdio } from "effect/Stdio";
7
8
  //#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
- }
24
- /** One Streamable HTTP MCP endpoint; its fields become the native server info. */
25
- export interface Options<Errors extends ReadonlyArray<Codec> = [], R = never> extends SurfaceOptions<Errors, R> {
26
- readonly name: string;
27
- readonly version: string;
28
- /** Native protocol adapters to serve; negotiation and sessions are owned by Effect. */
29
- readonly protocols: Parameters<typeof McpServer.layerHttp>[0]["protocols"];
30
- /** Route of the Streamable HTTP endpoint; no default. */
31
- readonly path: HttpRouter.PathInput;
32
- /** Browser origins accepted by the native server; passed through to `McpServer.layerHttp`. */
33
- readonly allowedOrigins?: ReadonlyArray<string>;
34
- readonly instructions?: string;
35
- }
36
- /** Server information for an MCP subprocess connected through standard I/O. */
37
- export interface StdioOptions<Errors extends ReadonlyArray<Codec> = [], R = never> extends SurfaceOptions<Errors, R> {
38
- readonly name: string;
39
- readonly version: string;
40
- /** Native protocol adapters to serve; negotiation is owned by Effect. */
41
- readonly protocols: Parameters<typeof McpServer.layerStdio>[0]["protocols"];
42
- readonly instructions?: string;
43
- }
9
+ /**
10
+ * One Streamable HTTP MCP endpoint: every native `McpServer.layerHttp` option except
11
+ * `protocols` (server information, `path`, `allowedOrigins`, `instructions`,
12
+ * `extensions`, ...), plus the surface's `errors` and `before`.
13
+ */
14
+ export interface Options<Errors extends ReadonlyArray<Codec> = [], R = never> extends Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols">, SurfaceOptions<Errors, R> {}
15
+ /** An MCP subprocess on standard I/O: every native `McpServer.layerStdio` option except `protocols`. */
16
+ export interface StdioOptions<Errors extends ReadonlyArray<Codec> = [], R = never> extends Omit<Parameters<typeof McpServer.layerStdio>[0], "protocols">, SurfaceOptions<Errors, R> {}
44
17
  /** The native server supplies its own request context to every tool call. */
45
- type ToolRequestContext<App, RB> = Exclude<RequestContext<App> | RB, McpSchema.McpRequestContext>;
18
+ type ToolRequestContext<App, RB> = Exclude<RequestContext<App, "mcp"> | RB, McpSchema.McpRequestContext>;
46
19
  /**
47
- * Serve MCP tools over one Streamable HTTP endpoint.
20
+ * Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28 only.
48
21
  *
49
22
  * Middleware provided around this layer has the normal HTTP lifetime. Native
50
23
  * context capture applies: never provide request-identity tags at startup.
51
24
  */
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>>>;
25
+ export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: Options<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], "mcp"> | Cause.IllegalArgumentError, BuildContext<Apps[number], "mcp"> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ToolRequestContext<Apps[number], RB>>>;
53
26
  /**
54
- * Serve MCP tools through newline-delimited JSON-RPC on standard I/O.
27
+ * Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
28
+ * 2026-07-28 only.
55
29
  *
56
30
  * The host supplies the `Stdio` service. Arguments are tool input only and
57
31
  * never establish request identity or authority.
58
32
  */
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>>;
33
+ export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: StdioOptions<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], "mcp"> | Cause.IllegalArgumentError, BuildContext<Apps[number], "mcp"> | Stdio | ToolRequestContext<Apps[number], RB>>;
60
34
  //#endregion
@@ -1,33 +1,29 @@
1
1
  import "./internal/implementation.mjs";
2
2
  import { bindTools } from "./internal/tools.mjs";
3
3
  import { Layer } from "effect";
4
- import { McpServer } from "effect/unstable/ai";
4
+ import { McpProtocol, McpServer } from "effect/unstable/ai";
5
5
  //#region src/ActionMcp.ts
6
+ /**
7
+ * The one protocol revision served. 2026-07-28 is stateless over HTTP: no
8
+ * initialize handshake and no session, so every request stands alone.
9
+ */
10
+ const protocols = [McpProtocol.v2026_07_28];
6
11
  const registration = (apps, options) => {
7
12
  const binding = bindTools(apps, "mcp", options);
8
13
  return Layer.effectDiscard(McpServer.registerToolkit(binding.toolkit)).pipe(Layer.provide(binding.layer));
9
14
  };
10
15
  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
- };
19
- function layerHttp(options, ...apps) {
20
- return server(apps, toolOptions(options), McpServer.layerHttp({
21
- name: options.name,
22
- version: options.version,
23
- instructions: options.instructions,
24
- path: options.path,
25
- protocols: options.protocols,
26
- allowedOrigins: options.allowedOrigins
16
+ function layerHttp(apps, options) {
17
+ return server(apps, options, McpServer.layerHttp({
18
+ ...options,
19
+ protocols
27
20
  }));
28
21
  }
29
- function layerStdio(options, ...apps) {
30
- return server(apps, toolOptions(options), McpServer.layerStdio(options));
22
+ function layerStdio(apps, options) {
23
+ return server(apps, options, McpServer.layerStdio({
24
+ ...options,
25
+ protocols
26
+ }));
31
27
  }
32
28
  //#endregion
33
29
  export { layerHttp, layerStdio };
@@ -1,23 +1,11 @@
1
1
  import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
2
  import { AnyImplementation, BuildContext, BuildError, HandlerContext, Implementation } from "./internal/implementation.mjs";
3
- import { Effect, Layer, Schema } from "effect";
3
+ import { SurfaceOptions } from "./internal/tools.mjs";
4
+ import { Layer, Schema } from "effect";
4
5
  import { Tool, Toolkit } from "effect/unstable/ai";
5
6
  //#region src/ActionToolkit.d.ts
6
- /** What `make` binds around the implementations it projects. */
7
- export interface Options<Errors extends ReadonlyArray<Codec> = [], R = never> {
8
- /**
9
- * Failures the caller answers with instead of a handler: authorization, rate
10
- * limits. Declared on every tool, so a refusal is returned as a tool failure
11
- * exactly like an action's own error.
12
- */
13
- readonly errors?: Errors;
14
- /**
15
- * Runs once per tool call, before the selected handler, with the action contract
16
- * it is about to run. It fails with this binding's `errors`. Its services join
17
- * each tool's request requirements, like a handler's.
18
- */
19
- readonly before?: (action: Any) => Effect.Effect<void, Errors[number]["Type"], R>;
20
- }
7
+ /** What the in-process caller binds around the implementations it projects. */
8
+ export type Options<Errors extends ReadonlyArray<Codec> = [], R = never> = SurfaceOptions<Errors, R>;
21
9
  /** A native tool corresponding to an MCP-enabled action. */
22
10
  type NativeTool<A extends Any, E extends Codec, R> = A extends {
23
11
  readonly mcp: {
@@ -49,5 +37,5 @@ export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R =
49
37
  * Build services are needed to construct `layer`; request services are needed
50
38
  * when the resulting toolkit handles a call.
51
39
  */
52
- export declare function make<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(options: Options<Errors, RB>, ...apps: Apps): Binding<ToolkitTools<Apps, Errors[number], RB>, BuildError<Apps[number]>, BuildContext<Apps[number]>>;
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], "mcp">, BuildContext<Apps[number], "mcp">>;
53
41
  //#endregion
@@ -3,13 +3,8 @@ import { bindTools } from "./internal/tools.mjs";
3
3
  import "effect";
4
4
  import "effect/unstable/ai";
5
5
  //#region src/ActionToolkit.ts
6
- function make(options, ...apps) {
7
- const before = options.before;
8
- const toolOptions = {
9
- errors: options.errors,
10
- before
11
- };
12
- return bindTools(apps, "native", toolOptions);
6
+ function make(apps, options = {}) {
7
+ return bindTools(apps, "native", options);
13
8
  }
14
9
  //#endregion
15
10
  export { make };
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Schema } from "effect";
2
2
  import { HttpApi, HttpApiClient, HttpApiGroup } from "effect/unstable/httpapi";
3
3
  //#region src/Testing.d.ts
4
4
  /** A web handler, such as `HttpRouter.toWebHandler(routes).handler`. */
@@ -33,4 +33,34 @@ export interface McpRequestParams {
33
33
  }
34
34
  /** Build one stateless 2026-07-28 JSON-RPC request, with client metadata defaulted. */
35
35
  export declare const mcpRequest: ({ url, method, params, headers: init }: McpRequestOptions) => Request;
36
+ /** One `tools/call` for `mcpCall`: the endpoint, the tool and its arguments. */
37
+ export interface McpCallOptions {
38
+ readonly url: string | URL;
39
+ /** The tool name, `mcp.name` of its action. */
40
+ readonly name: string;
41
+ /** Defaults to `{}`. Like `params`, it may be malformed on purpose. */
42
+ readonly arguments?: {
43
+ readonly [key: string]: McpRequestValue;
44
+ };
45
+ readonly headers?: ConstructorParameters<typeof Headers>[0];
46
+ }
47
+ /**
48
+ * A tool call's outcome with the library's wire envelope removed: the success from
49
+ * `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 (the native
51
+ * server's own message, such as for invalid arguments).
52
+ */
53
+ export type McpCallResult = {
54
+ readonly isError: false;
55
+ readonly value: Schema.Json;
56
+ } | {
57
+ readonly isError: true;
58
+ readonly error: Schema.Json;
59
+ };
60
+ /**
61
+ * Call one tool through `handler` with a stateless 2026-07-28 request and return its
62
+ * outcome. A response that is not an HTTP 200 carrying a tool result, such as an
63
+ * authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
64
+ */
65
+ export declare const mcpCall: (handler: Handler, { url, name, headers, ...options }: McpCallOptions) => Promise<McpCallResult>;
36
66
  //#endregion
package/dist/Testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, Layer, Predicate } from "effect";
1
+ import { Effect, Layer, Option, Predicate, Schema } from "effect";
2
2
  import { FetchHttpClient } from "effect/unstable/http";
3
3
  import { HttpApiClient } from "effect/unstable/httpapi";
4
4
  //#region src/Testing.ts
@@ -38,5 +38,53 @@ const mcpRequest = ({ url, method, params = {}, headers: init }) => {
38
38
  })
39
39
  });
40
40
  };
41
+ /** The JSON-RPC response to a `tools/call`: a tool result or a protocol error. */
42
+ const ToolReply = Schema.Union([Schema.Struct({ result: Schema.Struct({
43
+ isError: Schema.optionalKey(Schema.Boolean),
44
+ structuredContent: Schema.optionalKey(Schema.Struct({ value: Schema.Json })),
45
+ content: Schema.Array(Schema.Struct({
46
+ type: Schema.String,
47
+ text: Schema.optionalKey(Schema.String)
48
+ }))
49
+ }) }), Schema.Struct({ error: Schema.Struct({
50
+ code: Schema.Finite,
51
+ message: Schema.String
52
+ }) })]);
53
+ const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolReply));
54
+ const parseJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
55
+ /**
56
+ * Call one tool through `handler` with a stateless 2026-07-28 request and return its
57
+ * outcome. A response that is not an HTTP 200 carrying a tool result, such as an
58
+ * authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
59
+ */
60
+ const mcpCall = async (handler, { url, name, headers, ...options }) => {
61
+ const response = await handler(mcpRequest({
62
+ url,
63
+ method: "tools/call",
64
+ params: {
65
+ name,
66
+ arguments: options.arguments ?? {}
67
+ },
68
+ ...headers === void 0 ? {} : { headers }
69
+ }));
70
+ const body = await response.text();
71
+ if (response.status !== 200) throw new Error(`MCP tools/call "${name}" answered ${response.status}: ${body}`);
72
+ const reply = body.split("\n").map((line) => line.replace(/^data:/, "").trim()).flatMap((line) => Option.toArray(decodeReply(line))).at(-1);
73
+ if (reply === void 0) throw new Error(`MCP tools/call "${name}" had no reply: ${body}`);
74
+ if ("error" in reply) throw new Error(`MCP tools/call "${name}" failed with ${reply.error.code}: ${reply.error.message}`);
75
+ const { result } = reply;
76
+ if (result.isError === true) {
77
+ const text = result.content.find((content) => content.type === "text")?.text ?? "";
78
+ return {
79
+ isError: true,
80
+ error: Option.getOrElse(parseJson(text), () => text)
81
+ };
82
+ }
83
+ if (result.structuredContent === void 0) throw new Error(`MCP tools/call "${name}" returned no structured content: ${body}`);
84
+ return {
85
+ isError: false,
86
+ value: result.structuredContent.value
87
+ };
88
+ };
41
89
  //#endregion
42
- export { httpClient, mcpRequest };
90
+ export { httpClient, mcpCall, mcpRequest };
@@ -1,16 +1,17 @@
1
- import { Client, ClientOptions } from "@modelcontextprotocol/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
- /** Connect an official client and always close its transport after the callback. */
15
- export declare const withMcpClient: <A>({ fetch, path, baseUrl, headers, versionNegotiation }: McpClientOptions, run: (client: Client) => Promise<A>) => Promise<A>;
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
@@ -1,11 +1,14 @@
1
1
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
2
2
  //#region src/TestingClient.ts
3
- /** Connect an official client and always close its transport after the callback. */
4
- const withMcpClient = async ({ fetch, path, baseUrl = "http://localhost", headers, versionNegotiation }, run) => {
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 === void 0 ? {} : { 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
- /** Pure HTTP policy for native schema decoding and encoding failures. */
5
- export interface SchemaErrorPolicy<Errors extends ReadonlyArray<Codec>> {
6
- readonly errors: Errors;
7
- readonly map: (failure: HttpApiError.HttpApiSchemaError) => NoInfer<Errors[number]["Type"]>;
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>, PolicyErrors extends ReadonlyArray<Codec> = ReadonlyArray<Codec>> {
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<PolicyErrors> | undefined;
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"]>["errors"][number];
28
+ export type PolicyError<G extends Actions> = NonNullable<G["schemaError"]>[keyof SchemaErrorPolicy]["schema"];
18
29
  //#endregion
@@ -1,15 +1,34 @@
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
5
15
  * not repeated.
6
16
  */
7
- const projectedErrors = (action, surface) => [...action.errors, ...(surface ?? []).filter((error) => !action.errors.includes(error))];
17
+ const projectedErrors = (action, surface) => [.../* @__PURE__ */ new Set([...action.errors, ...surface ?? []])];
8
18
  const validName = /^[A-Za-z0-9_-]+$/;
9
19
  /** Names become path segments, OpenAPI identifiers and client method keys. */
10
20
  const assertName = (what, name) => {
11
21
  if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
12
22
  };
23
+ /**
24
+ * The one item of `items` named `name`, narrowed to that contract. A caller passes
25
+ * a literal name, so a miss is a programming error reported as `Unknown <what>`.
26
+ */
27
+ const selectNamed = (items, name, what) => {
28
+ const item = items.find((candidate) => candidate.name === name);
29
+ if (item === void 0) throw new Error(`Unknown ${what}`);
30
+ return item;
31
+ };
13
32
  /** Each namespace is checked by whoever owns it: a group, the routes, or the MCP tools. */
14
33
  const assertDistinct = (what, names) => {
15
34
  const seen = /* @__PURE__ */ new Set();
@@ -19,4 +38,4 @@ const assertDistinct = (what, names) => {
19
38
  }
20
39
  };
21
40
  //#endregion
22
- export { assertDistinct, assertName, projectedErrors };
41
+ export { answerSchemaError, assertDistinct, assertName, policyErrors, projectedErrors, selectNamed };
@@ -6,7 +6,7 @@ import { Command } from "effect/unstable/cli";
6
6
  interface CommonOptions<Output> {
7
7
  /** Override the command name. The action name is used by default. */
8
8
  readonly name?: string;
9
- /** Human output. JSON remains available with `--json`. */
9
+ /** Human output. Adds a `--json` flag to the command that selects JSON instead. */
10
10
  readonly render?: (output: Output) => string;
11
11
  }
12
12
  /** The default projection accepts a whole canonical JSON input with `--input`. */
@@ -19,7 +19,7 @@ export interface ParametersOptions<Output, Parameters extends Command.Command.Co
19
19
  /** Native Effect CLI flags and arguments. */
20
20
  readonly parameters: Parameters;
21
21
  /** Maps native parsed parameters to the action's canonical JSON input. */
22
- readonly input: (parsed: Command.Command.Config.Infer<Parameters>) => Schema.Json;
22
+ readonly input: (parsed: Command.Command.Config.InferValue<Parameters>) => Schema.Json;
23
23
  }
24
24
  /**
25
25
  * A command either accepts whole JSON input or has explicit native CLI parameters.
@@ -2,31 +2,45 @@ import { Console, Effect, Option, Schema } from "effect";
2
2
  import { Command, Flag } from "effect/unstable/cli";
3
3
  //#region src/internal/cli.ts
4
4
  const isParametersOptions = (options) => options?.parameters !== void 0;
5
- const jsonOutput = Flag.Boolean("json").pipe(Flag.optional, Flag.withDescription("Print machine-readable JSON"));
6
- const output = (action, execute, input, render, useJson) => Effect.gen(function* () {
5
+ /**
6
+ * A command with a renderer takes `--json` as a flag of its own, so nothing is
7
+ * claimed tree-wide and a host's `--json`, global or not, is never contested.
8
+ * Without a renderer the output is JSON already and there is no flag.
9
+ */
10
+ const jsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
11
+ const output = (action, execute, input, render) => Effect.gen(function* () {
7
12
  const value = yield* execute(input);
8
13
  const encoded = yield* Schema.encodeEffect(Schema.toCodecJson(action.success))(value);
9
- const text = render !== void 0 && !useJson ? render(value) : JSON.stringify(encoded, null, 2);
10
- yield* Console.log(text);
14
+ yield* Console.log(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value));
11
15
  });
12
- const defaultCommand = (action, execute, options) => {
13
- const input = Flag.String("input").pipe(Flag.withDefault("{}"), Flag.withSchema(Schema.fromJsonString(Schema.toCodecJson(action.input))), Flag.withDescription("Whole canonical action input as JSON"));
14
- const config = options?.render === void 0 ? { input } : {
15
- input,
16
- output: { json: jsonOutput }
17
- };
18
- return Command.make(options?.name ?? action.name, config, (parsed) => Effect.gen(function* () {
19
- const useJson = "output" in parsed && Option.isSome(parsed.output.json) && parsed.output.json.value === true;
20
- return yield* output(action, execute, parsed.input, options?.render, useJson);
21
- })).pipe(Command.withDescription(action.description));
16
+ /**
17
+ * One native command. The action's own parameters are one nested config, so the
18
+ * flag added for a renderer never meets them in one record.
19
+ */
20
+ const make = (action, execute, options, config, decode) => {
21
+ const name = options?.name ?? action.name;
22
+ const render = options?.render;
23
+ return (render === void 0 ? Command.make(name, { parameters: config }, ({ parameters }) => Effect.flatMap(decode(parameters), (input) => output(action, execute, input, void 0))) : Command.make(name, {
24
+ parameters: config,
25
+ json: jsonFlag
26
+ }, ({ parameters, json }) => Effect.flatMap(decode(parameters), (input) => output(action, execute, input, json ? void 0 : render)))).pipe(Command.withDescription(action.description));
22
27
  };
23
- const parametersCommand = (action, execute, options) => {
24
- if (options.render === void 0) return Command.make(options.name ?? action.name, options.parameters, (parsed) => Effect.flatMap(Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))(options.input(parsed)), (input) => output(action, execute, input, void 0, true))).pipe(Command.withDescription(action.description));
25
- return Command.make(options.name ?? action.name, {
26
- parameters: options.parameters,
27
- output: { json: jsonOutput }
28
- }, (parsed) => Effect.flatMap(Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))(options.input(parsed.parameters)), (input) => output(action, execute, input, options.render, Option.isSome(parsed.output.json) && parsed.output.json.value === true))).pipe(Command.withDescription(action.description));
28
+ /**
29
+ * The whole encoded input, inline or from a file. The native parser decodes both,
30
+ * so an invalid value is rendered with the command's help wherever it came from.
31
+ */
32
+ const inputFlags = (codec) => ({
33
+ input: Flag.String("input").pipe(Flag.withSchema(Schema.fromJsonString(codec)), Flag.optional, Flag.withDescription("Whole canonical action input as JSON")),
34
+ inputFile: Flag.FileSchema("input-file", codec, { format: "json" }).pipe(Flag.optional, Flag.withDescription("File containing the whole canonical action input as JSON"))
35
+ });
36
+ const defaultCommand = (action, execute, options) => {
37
+ const codec = Schema.toCodecJson(action.input);
38
+ return make(action, execute, options, inputFlags(codec), (parsed) => Option.match(Option.orElse(parsed.inputFile, () => parsed.input), {
39
+ onNone: () => Schema.decodeUnknownEffect(codec)({}),
40
+ onSome: Effect.succeed
41
+ }));
29
42
  };
43
+ const parametersCommand = (action, execute, options) => make(action, execute, options, options.parameters, (parsed) => Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))(options.input(parsed)));
30
44
  /** Build one native command around an action-bound operation. */
31
45
  const command = (action, execute, options) => {
32
46
  if (isParametersOptions(options)) return parametersCommand(action, execute, options);
@@ -30,10 +30,21 @@ 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
- /** Per-request requirements of one implementation, or of a union of them. */
34
- export type RequestContext<App> = App extends Implementation<any, infer H, any, any> ? HandlersContext<H> : never;
35
- /** Handler-acquisition failures of one implementation, or of a union of them. */
36
- export type BuildError<App> = App extends Implementation<any, any, infer EX, any> ? EX : never;
37
- /** Handler-acquisition requirements of one implementation, or of a union of them. */
38
- export type BuildContext<App> = App extends Implementation<any, any, any, infer RX> ? RX : never;
33
+ /** The transports that serve a subset of a group's actions, by the contract flag that hides an action. */
34
+ export type Transport = "http" | "mcp";
35
+ /**
36
+ * Names of the actions of `G` that `T` may serve. Only an action hidden by a
37
+ * literal `false` is excluded; a flag decided at runtime keeps its requirements.
38
+ */
39
+ export type ServedNames<G extends Actions, T extends Transport> = Exclude<G["actions"][number], T extends "http" ? {
40
+ readonly http: false;
41
+ } : {
42
+ readonly mcp: false;
43
+ }>["name"];
44
+ /** Per-request requirements of the handlers `T` can invoke, or of a union of implementations. */
45
+ export type RequestContext<App, T extends Transport> = App extends Implementation<infer G, infer H, any, any> ? { readonly [K in Extract<ServedNames<G, T>, keyof H>]: HandlerContext<H, K>; }[Extract<ServedNames<G, T>, keyof H>] : never;
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;
39
50
  //#endregion
@@ -22,8 +22,9 @@ var Implementation = class Implementation {
22
22
  }
23
23
  };
24
24
  /**
25
- * Select and invoke a handler after an adapter has selected a concrete action.
26
- * The surface's pre-handler hook runs first, outside the action's span, so a
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.
@@ -31,8 +32,16 @@ var Implementation = class Implementation {
31
32
  const dispatch = (group, action, handlers, before) => {
32
33
  const handle = handlers[action.name];
33
34
  if (handle === void 0) throw new Error(`Missing handler: ${action.name}`);
35
+ const attributes = {
36
+ "action.group": group.name,
37
+ "action.name": action.name,
38
+ "action.access": action.access
39
+ };
34
40
  return (input) => {
35
- const handled = Effect.withSpan(Effect.suspend(() => handle(input)), `${group.name}.${action.name}`, { captureStackTrace: false });
41
+ const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), attributes), `${group.name}.${action.name}`, {
42
+ captureStackTrace: false,
43
+ attributes
44
+ });
36
45
  return before === void 0 ? handled : Effect.flatMap(before(action), () => handled);
37
46
  };
38
47
  };
@@ -0,0 +1,22 @@
1
+ import { Action_d_exports, Any, Codec } from "../Action.mjs";
2
+ import "./implementation.mjs";
3
+ import { Effect, Layer } from "effect";
4
+ import { Tool, Toolkit } from "effect/unstable/ai";
5
+ //#region src/internal/tools.d.ts
6
+ /** What a tool surface binds around the implementations it projects. */
7
+ export interface SurfaceOptions<Errors extends ReadonlyArray<Codec>, R> {
8
+ /**
9
+ * Failures the surface answers with instead of a handler: authorization, rate
10
+ * limits. Declared on every tool, so a refusal is returned exactly like an
11
+ * action's own error.
12
+ */
13
+ readonly errors?: Errors;
14
+ /**
15
+ * Runs once per tool call, after the arguments are decoded and before the
16
+ * selected handler, with the action contract it is about to run. It fails with
17
+ * this surface's `errors`. Its services are request-time requirements, like a
18
+ * handler's.
19
+ */
20
+ readonly before?: (action: Any) => Effect.Effect<void, Errors[number]["Type"], R>;
21
+ }
22
+ //#endregion