@gjermundgaraba/effect-actions 0.1.0-rc.0 → 0.1.0-rc.2
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 +249 -75
- package/dist/Action.d.mts +35 -27
- package/dist/Action.mjs +9 -10
- package/dist/ActionCatalog.d.mts +31 -0
- package/dist/ActionCatalog.mjs +36 -0
- package/dist/ActionCli.d.mts +27 -0
- package/dist/ActionCli.mjs +26 -0
- package/dist/ActionCliClient.d.mts +45 -0
- package/dist/ActionCliClient.mjs +52 -0
- package/dist/ActionGroup.d.mts +25 -16
- package/dist/ActionGroup.mjs +18 -19
- package/dist/ActionHttp.d.mts +39 -0
- package/dist/ActionHttp.mjs +67 -0
- package/dist/ActionMcp.d.mts +41 -0
- package/dist/ActionMcp.mjs +25 -0
- package/dist/ActionToolkit.d.mts +37 -0
- package/dist/ActionToolkit.mjs +10 -0
- package/dist/Authentication.d.mts +46 -0
- package/dist/Authentication.mjs +55 -0
- package/dist/Testing.d.mts +36 -0
- package/dist/Testing.mjs +42 -0
- package/dist/TestingClient.d.mts +16 -0
- package/dist/{testing/client.mjs → TestingClient.mjs} +5 -6
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → _virtual/_rolldown/runtime.mjs} +1 -1
- package/dist/internal/actions.d.mts +18 -0
- package/dist/internal/actions.mjs +16 -0
- package/dist/internal/cli.d.mts +29 -0
- package/dist/internal/cli.mjs +36 -0
- package/dist/internal/implementation.d.mts +39 -0
- package/dist/internal/implementation.mjs +37 -0
- package/dist/internal/tools.mjs +84 -0
- package/package.json +16 -12
- package/dist/authentication.d.mts +0 -27
- package/dist/authentication.mjs +0 -31
- package/dist/http.d.mts +0 -53
- package/dist/http.mjs +0 -74
- package/dist/implementation-BkLqMWtv.mjs +0 -32
- package/dist/implementation-D75QAcD3.d.mts +0 -21
- package/dist/index.d.mts +0 -6
- package/dist/index.mjs +0 -6
- package/dist/mcp.d.mts +0 -46
- package/dist/mcp.mjs +0 -189
- package/dist/testing/client.d.mts +0 -11
- package/dist/testing.d.mts +0 -10
- package/dist/testing.mjs +0 -34
package/dist/Testing.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect, Layer, Predicate } from "effect";
|
|
2
|
+
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
3
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
4
|
+
//#region src/Testing.ts
|
|
5
|
+
/**
|
|
6
|
+
* The native grouped HTTP client, calling `handler` in memory instead of the network.
|
|
7
|
+
* `baseUrl` defaults to `http://localhost`.
|
|
8
|
+
*/
|
|
9
|
+
const httpClient = (api, handler, options) => HttpApiClient.make(api, {
|
|
10
|
+
baseUrl: "http://localhost",
|
|
11
|
+
...options
|
|
12
|
+
}).pipe(Effect.provide(FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, (input, init) => handler(new Request(input, init)))))));
|
|
13
|
+
/** Build one stateless 2026-07-28 JSON-RPC request, with client metadata defaulted. */
|
|
14
|
+
const mcpRequest = ({ url, method, params = {}, headers: init }) => {
|
|
15
|
+
const headers = new Headers(init);
|
|
16
|
+
headers.set("content-type", "application/json");
|
|
17
|
+
headers.set("accept", "application/json, text/event-stream");
|
|
18
|
+
headers.set("mcp-protocol-version", "2026-07-28");
|
|
19
|
+
headers.set("mcp-method", method);
|
|
20
|
+
if (Predicate.isString(params.name)) headers.set("mcp-name", params.name);
|
|
21
|
+
return new Request(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers,
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
jsonrpc: "2.0",
|
|
26
|
+
id: 1,
|
|
27
|
+
method,
|
|
28
|
+
params: {
|
|
29
|
+
...params,
|
|
30
|
+
_meta: Object.assign({
|
|
31
|
+
"io.modelcontextprotocol/clientCapabilities": {},
|
|
32
|
+
"io.modelcontextprotocol/clientInfo": {
|
|
33
|
+
name: "test",
|
|
34
|
+
version: "0"
|
|
35
|
+
}
|
|
36
|
+
}, params._meta, { "io.modelcontextprotocol/protocolVersion": "2026-07-28" })
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { httpClient, mcpRequest };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Client, ClientOptions } from "@modelcontextprotocol/client";
|
|
2
|
+
//#region src/TestingClient.d.ts
|
|
3
|
+
/** How `withMcpClient` connects the official client. */
|
|
4
|
+
export interface McpClientOptions {
|
|
5
|
+
/** A web handler, such as `HttpRouter.toWebHandler(routes).handler`. */
|
|
6
|
+
readonly fetch: (request: Request) => Promise<Response>;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
/** Passed directly to the official client; omitted options use its native default. */
|
|
9
|
+
readonly versionNegotiation?: ClientOptions["versionNegotiation"];
|
|
10
|
+
/** Defaults to `http://localhost`. */
|
|
11
|
+
readonly baseUrl?: string | URL;
|
|
12
|
+
readonly headers?: ConstructorParameters<typeof Headers>[0];
|
|
13
|
+
}
|
|
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>;
|
|
16
|
+
//#endregion
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
2
2
|
//#region src/TestingClient.ts
|
|
3
3
|
/** Connect an official client and always close its transport after the callback. */
|
|
4
|
-
const withMcpClient = async (
|
|
4
|
+
const withMcpClient = async ({ fetch, path, baseUrl = "http://localhost", headers, versionNegotiation }, run) => {
|
|
5
5
|
const client = new Client({
|
|
6
6
|
name: "test",
|
|
7
7
|
version: "0"
|
|
8
|
-
},
|
|
8
|
+
}, versionNegotiation === void 0 ? {} : { versionNegotiation });
|
|
9
9
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}));
|
|
10
|
+
const transportOptions = { fetch: (input, init) => fetch(new Request(input, init)) };
|
|
11
|
+
if (headers !== void 0) transportOptions.requestInit = { headers };
|
|
12
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(path, baseUrl), transportOptions));
|
|
14
13
|
return await run(client);
|
|
15
14
|
} finally {
|
|
16
15
|
await client.close();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Action_d_exports, Any, Codec } from "../Action.mjs";
|
|
2
|
+
import { HttpApiError } from "effect/unstable/httpapi";
|
|
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"]>;
|
|
8
|
+
}
|
|
9
|
+
/** 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>> {
|
|
11
|
+
readonly name: Name;
|
|
12
|
+
readonly actions: A;
|
|
13
|
+
/** How HTTP answers failed decoding or encoding; native behavior without one. MCP is unaffected. */
|
|
14
|
+
readonly schemaError: SchemaErrorPolicy<PolicyErrors> | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** 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];
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/internal/actions.ts
|
|
2
|
+
const validName = /^[A-Za-z0-9_-]+$/;
|
|
3
|
+
/** Names become path segments, OpenAPI identifiers and client method keys. */
|
|
4
|
+
const assertName = (what, name) => {
|
|
5
|
+
if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
|
|
6
|
+
};
|
|
7
|
+
/** Each namespace is checked by whoever owns it: a group, the routes, or the MCP tools. */
|
|
8
|
+
const assertDistinct = (what, names) => {
|
|
9
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
if (seen.has(name)) throw new Error(`Duplicate ${what}: ${name}`);
|
|
12
|
+
seen.add(name);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { assertDistinct, assertName };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "../Action.mjs";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Command } from "effect/unstable/cli";
|
|
4
|
+
//#region src/internal/cli.d.ts
|
|
5
|
+
/** Options shared by the local and HTTP command projections. */
|
|
6
|
+
interface CommonOptions<Output> {
|
|
7
|
+
/** Override the command name. The action name is used by default. */
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
/** Human output. JSON remains available with `--json`. */
|
|
10
|
+
readonly render?: (output: Output) => string;
|
|
11
|
+
}
|
|
12
|
+
/** The default projection accepts a whole canonical JSON input with `--input`. */
|
|
13
|
+
export interface JsonOptions<Output> extends CommonOptions<Output> {
|
|
14
|
+
readonly parameters?: never;
|
|
15
|
+
readonly input?: never;
|
|
16
|
+
}
|
|
17
|
+
/** Supply native CLI parameters and map their parsed values to canonical action JSON. */
|
|
18
|
+
export interface ParametersOptions<Output, Parameters extends Command.Command.Config> extends CommonOptions<Output> {
|
|
19
|
+
/** Native Effect CLI flags and arguments. */
|
|
20
|
+
readonly parameters: Parameters;
|
|
21
|
+
/** Maps native parsed parameters to the action's canonical JSON input. */
|
|
22
|
+
readonly input: (parsed: Command.Command.Config.Infer<Parameters>) => Schema.Json;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A command either accepts whole JSON input or has explicit native CLI parameters.
|
|
26
|
+
* The latter intentionally has no implicit `--input` mode.
|
|
27
|
+
*/
|
|
28
|
+
export type Options<Output, Parameters extends Command.Command.Config = never> = JsonOptions<Output> | ParametersOptions<Output, Parameters>;
|
|
29
|
+
//#endregion
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Console, Effect, Option, Schema } from "effect";
|
|
2
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
//#region src/internal/cli.ts
|
|
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* () {
|
|
7
|
+
const value = yield* execute(input);
|
|
8
|
+
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);
|
|
11
|
+
});
|
|
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));
|
|
22
|
+
};
|
|
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));
|
|
29
|
+
};
|
|
30
|
+
/** Build one native command around an action-bound operation. */
|
|
31
|
+
const command = (action, execute, options) => {
|
|
32
|
+
if (isParametersOptions(options)) return parametersCommand(action, execute, options);
|
|
33
|
+
return defaultCommand(action, execute, options);
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { command };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Action_d_exports, Any } from "../Action.mjs";
|
|
2
|
+
import { Actions } from "./actions.mjs";
|
|
3
|
+
import { Effect, Scope } from "effect";
|
|
4
|
+
//#region src/internal/implementation.d.ts
|
|
5
|
+
/** Decoded values at the adapter dispatch boundary. */
|
|
6
|
+
export type ErasedValue = Any["input"]["Type"];
|
|
7
|
+
type ErasedHandler<R> = {
|
|
8
|
+
handle(input: ErasedValue): Effect.Effect<ErasedValue, ErasedValue, R>;
|
|
9
|
+
}["handle"];
|
|
10
|
+
/** An adapter's erased view of a record of handlers with request requirements `R`. */
|
|
11
|
+
export type Handlers<R> = Readonly<Record<string, ErasedHandler<R>>>;
|
|
12
|
+
/** Per-request requirements of the handler selected by an action name. */
|
|
13
|
+
export type HandlerContext<H, Name extends keyof H> = H[Name] extends ((input: never) => Effect.Effect<infer _A, infer _E, infer R>) ? R : never;
|
|
14
|
+
/** The union of per-request requirements declared by a handler record. */
|
|
15
|
+
export type HandlersContext<H> = { readonly [K in keyof H]: HandlerContext<H, K>; }[keyof H];
|
|
16
|
+
/**
|
|
17
|
+
* An acquired handler record for a group.
|
|
18
|
+
*
|
|
19
|
+
* The private field makes this class nominal: a structurally similar object,
|
|
20
|
+
* including one made by spreading an implementation, is not an implementation.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Implementation<G extends Actions, H, EX, RX> {
|
|
23
|
+
#private;
|
|
24
|
+
/** The group whose exact handler record was built. */
|
|
25
|
+
readonly group: G;
|
|
26
|
+
private constructor();
|
|
27
|
+
/** Acquire the exact handler record in the adapter layer's scope. */
|
|
28
|
+
get build(): Effect.Effect<H, EX, RX | Scope.Scope>;
|
|
29
|
+
static make<G extends Actions, H, EX, RX>(group: G, build: Effect.Effect<H, EX, RX | Scope.Scope>): Implementation<G, H, EX, RX>;
|
|
30
|
+
}
|
|
31
|
+
/** Any nominal implementation of `G`, with its record and channels erased. */
|
|
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;
|
|
39
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
//#region src/internal/implementation.ts
|
|
3
|
+
/**
|
|
4
|
+
* An acquired handler record for a group.
|
|
5
|
+
*
|
|
6
|
+
* The private field makes this class nominal: a structurally similar object,
|
|
7
|
+
* including one made by spreading an implementation, is not an implementation.
|
|
8
|
+
*/
|
|
9
|
+
var Implementation = class Implementation {
|
|
10
|
+
group;
|
|
11
|
+
#build;
|
|
12
|
+
constructor(group, build) {
|
|
13
|
+
this.group = group;
|
|
14
|
+
this.#build = build;
|
|
15
|
+
}
|
|
16
|
+
/** Acquire the exact handler record in the adapter layer's scope. */
|
|
17
|
+
get build() {
|
|
18
|
+
return this.#build;
|
|
19
|
+
}
|
|
20
|
+
static make(group, build) {
|
|
21
|
+
return new Implementation(group, build);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Select and invoke a handler after an adapter has selected a concrete action.
|
|
26
|
+
* `R` remains in the returned effect so transport layers cannot erase required
|
|
27
|
+
* per-request services while assembling routes.
|
|
28
|
+
*/
|
|
29
|
+
const dispatch = (group, action, handlers) => {
|
|
30
|
+
const handle = handlers[action.name];
|
|
31
|
+
if (handle === void 0) throw new Error(`Missing handler: ${action.name}`);
|
|
32
|
+
return (input) => {
|
|
33
|
+
return Effect.withSpan(Effect.suspend(() => handle(input)), `${group.name}.${action.name}`, { captureStackTrace: false });
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { Implementation, dispatch };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { dispatch } from "./implementation.mjs";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
|
+
//#region src/internal/tools.ts
|
|
5
|
+
const isToolAction = (action) => action.mcp !== false;
|
|
6
|
+
/** Only tool-enabled groups are acquired; each implementation appears once. */
|
|
7
|
+
const toolApps = (apps) => apps.flatMap((app) => {
|
|
8
|
+
const actions = app.group.actions.filter(isToolAction);
|
|
9
|
+
return actions.length === 0 ? [] : [{
|
|
10
|
+
app,
|
|
11
|
+
actions
|
|
12
|
+
}];
|
|
13
|
+
});
|
|
14
|
+
/** Flatten the projected contracts only after deciding which groups are served. */
|
|
15
|
+
const entries = (apps) => apps.flatMap(({ app, actions }) => actions.map((action) => ({
|
|
16
|
+
app,
|
|
17
|
+
action
|
|
18
|
+
})));
|
|
19
|
+
/** Fail before acquiring handlers when two exposed tools share a name. */
|
|
20
|
+
const assertDistinctToolNames = (tools) => {
|
|
21
|
+
const names = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const { action } of tools) {
|
|
23
|
+
const name = action.mcp.name;
|
|
24
|
+
if (names.has(name)) throw new Error(`Duplicate MCP tool: ${name}`);
|
|
25
|
+
names.add(name);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const annotate = (tool, mcp) => tool.annotate(Tool.Readonly, mcp.readOnly).annotate(Tool.Destructive, mcp.destructive).annotate(Tool.Idempotent, mcp.idempotent).annotate(Tool.OpenWorld, mcp.openWorld);
|
|
29
|
+
/**
|
|
30
|
+
* A native Effect AI tool. Its schemas retain action transforms and its result
|
|
31
|
+
* is the action result itself, rather than an MCP response envelope.
|
|
32
|
+
*/
|
|
33
|
+
const nativeTool = (action) => annotate(Tool.make(action.mcp.name, {
|
|
34
|
+
description: action.description,
|
|
35
|
+
parameters: action.input,
|
|
36
|
+
success: action.success,
|
|
37
|
+
failure: Schema.Union(action.errors),
|
|
38
|
+
failureMode: "return"
|
|
39
|
+
}), action.mcp);
|
|
40
|
+
/**
|
|
41
|
+
* MCP has a JSON-only wire contract. Success uses its documented `{ value }`
|
|
42
|
+
* structured-content envelope; declared failures are returned as JSON text.
|
|
43
|
+
*/
|
|
44
|
+
const mcpTool = (action) => annotate(Tool.make(action.mcp.name, {
|
|
45
|
+
description: action.description,
|
|
46
|
+
parameters: Schema.toCodecJson(action.input),
|
|
47
|
+
success: Schema.toCodecJson(Schema.Struct({ value: action.success })),
|
|
48
|
+
failure: Schema.toCodecJson(Schema.Union(action.errors)),
|
|
49
|
+
failureMode: "return"
|
|
50
|
+
}), action.mcp);
|
|
51
|
+
/** MCP's input schema must describe a JSON object, including no-argument tools. */
|
|
52
|
+
const assertMcpObjectInput = (action, tool) => {
|
|
53
|
+
const root = Tool.getJsonSchemaFromSchema(tool.parametersSchema);
|
|
54
|
+
if (root.$ref === void 0 && root.type !== "object") throw new Error(`${action.name}: MCP input must have an object root; omit input for no arguments`);
|
|
55
|
+
};
|
|
56
|
+
const project = (projection, entry) => {
|
|
57
|
+
const tool = projection === "native" ? nativeTool(entry.action) : mcpTool(entry.action);
|
|
58
|
+
if (projection === "mcp") assertMcpObjectInput(entry.action, tool);
|
|
59
|
+
return tool;
|
|
60
|
+
};
|
|
61
|
+
const handler = (projection, app, action, handlers) => {
|
|
62
|
+
const run = (input) => dispatch(app.group, action, handlers)(input);
|
|
63
|
+
return projection === "native" ? run : (input) => Effect.map(run(input), (value) => ({ value }));
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Project MCP-enabled actions and acquire their handlers exactly once per adapter layer.
|
|
67
|
+
* Native and MCP differ only in tool codecs and the MCP success envelope; selection,
|
|
68
|
+
* scoped acquisition, dispatch and native Toolkit binding stay identical.
|
|
69
|
+
*/
|
|
70
|
+
const bindTools = (apps, projection) => {
|
|
71
|
+
const served = toolApps(apps);
|
|
72
|
+
const selected = entries(served);
|
|
73
|
+
assertDistinctToolNames(selected);
|
|
74
|
+
const toolkit = Toolkit.make(...selected.map((entry) => project(projection, entry)));
|
|
75
|
+
return {
|
|
76
|
+
toolkit,
|
|
77
|
+
layer: toolkit.toLayer(Effect.map(Effect.forEach(served, ({ app, actions }) => Effect.map(app.build, (handlers) => {
|
|
78
|
+
const record = handlers;
|
|
79
|
+
return actions.map((action) => [action.mcp.name, handler(projection, app, action, record)]);
|
|
80
|
+
})), (built) => Object.fromEntries(built.flat())))
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { bindTools };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
4
|
-
"description": "Define Effect action contracts once and
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
|
+
"description": "Define Effect action contracts once and project them onto HTTP, MCP, native Toolkits, catalogs, and CLIs.",
|
|
5
5
|
"homepage": "https://github.com/gjermundgaraba/effect-actions#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/gjermundgaraba/effect-actions/issues"
|
|
@@ -16,24 +16,28 @@
|
|
|
16
16
|
],
|
|
17
17
|
"type": "module",
|
|
18
18
|
"exports": {
|
|
19
|
-
".": "./dist/index.mjs",
|
|
20
19
|
"./Action": "./dist/Action.mjs",
|
|
20
|
+
"./ActionCatalog": "./dist/ActionCatalog.mjs",
|
|
21
|
+
"./ActionCli": "./dist/ActionCli.mjs",
|
|
22
|
+
"./ActionCliClient": "./dist/ActionCliClient.mjs",
|
|
21
23
|
"./ActionGroup": "./dist/ActionGroup.mjs",
|
|
22
|
-
"./
|
|
23
|
-
"./
|
|
24
|
-
"./
|
|
25
|
-
"./
|
|
26
|
-
"./
|
|
24
|
+
"./ActionHttp": "./dist/ActionHttp.mjs",
|
|
25
|
+
"./ActionMcp": "./dist/ActionMcp.mjs",
|
|
26
|
+
"./ActionToolkit": "./dist/ActionToolkit.mjs",
|
|
27
|
+
"./Authentication": "./dist/Authentication.mjs",
|
|
28
|
+
"./Testing": "./dist/Testing.mjs",
|
|
29
|
+
"./TestingClient": "./dist/TestingClient.mjs",
|
|
27
30
|
"./package.json": "./package.json"
|
|
28
31
|
},
|
|
29
32
|
"publishConfig": {
|
|
30
|
-
"access": "public"
|
|
33
|
+
"access": "public",
|
|
34
|
+
"tag": "rc"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
33
|
-
"@effect/platform-node": "
|
|
37
|
+
"@effect/platform-node": "4.0.0-rc.116",
|
|
34
38
|
"@modelcontextprotocol/client": "2.0.0",
|
|
35
39
|
"@types/node": "26.5.1",
|
|
36
|
-
"effect": "
|
|
40
|
+
"effect": "4.0.0-rc.116",
|
|
37
41
|
"tsx": "4.23.13",
|
|
38
42
|
"typescript": "7.0.2",
|
|
39
43
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.2",
|
|
@@ -41,7 +45,7 @@
|
|
|
41
45
|
},
|
|
42
46
|
"peerDependencies": {
|
|
43
47
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
44
|
-
"effect": "4.0.0-rc.
|
|
48
|
+
"effect": "4.0.0-rc.116"
|
|
45
49
|
},
|
|
46
50
|
"peerDependenciesMeta": {
|
|
47
51
|
"@modelcontextprotocol/client": {
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Codec, t as Action_d_exports } from "./Action.mjs";
|
|
2
|
-
import { Context, Effect } from "effect";
|
|
3
|
-
import { Headers, HttpRouter } from "effect/unstable/http";
|
|
4
|
-
declare namespace Authentication_d_exports {
|
|
5
|
-
export { Options, middleware };
|
|
6
|
-
}
|
|
7
|
-
export interface Options<A, Errors extends ReadonlyArray<Codec>, R> {
|
|
8
|
-
readonly authenticate: Effect.Effect<A, Errors[number]["Type"], R>;
|
|
9
|
-
readonly errors: Errors;
|
|
10
|
-
readonly headers?: (error: Errors[number]["Type"]) => Headers.Input;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Authenticate each request and provide its identity to the downstream handler.
|
|
14
|
-
* Only authentication failures are encoded, using each schema's httpApiStatus.
|
|
15
|
-
* Dependencies remain native router request requirements. Acquired resources live
|
|
16
|
-
* until the request scope closes, including while the handler is running.
|
|
17
|
-
*/
|
|
18
|
-
export declare const middleware: <I, A, const Errors extends ReadonlyArray<Codec>, R>(service: Context.Key<I, A>, options: Options<NoInfer<A>, Errors, R>) => HttpRouter.Middleware<{
|
|
19
|
-
provides: I;
|
|
20
|
-
handles: never;
|
|
21
|
-
error: never;
|
|
22
|
-
requires: Exclude<R, HttpRouter.Provided> | Exclude<Exclude<import("effect/Types").NoInfer<I>, I>, HttpRouter.Provided>;
|
|
23
|
-
layerError: never;
|
|
24
|
-
layerRequires: never;
|
|
25
|
-
}>;
|
|
26
|
-
//#endregion
|
|
27
|
-
export { Authentication_d_exports as t };
|
package/dist/authentication.mjs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import { Effect, Schema, SchemaAST } from "effect";
|
|
3
|
-
import { HttpEffect, HttpRouter, HttpServerResponse } from "effect/unstable/http";
|
|
4
|
-
//#region src/Authentication.ts
|
|
5
|
-
var Authentication_exports = /* @__PURE__ */ __exportAll({ middleware: () => middleware });
|
|
6
|
-
/**
|
|
7
|
-
* Authenticate each request and provide its identity to the downstream handler.
|
|
8
|
-
* Only authentication failures are encoded, using each schema's httpApiStatus.
|
|
9
|
-
* Dependencies remain native router request requirements. Acquired resources live
|
|
10
|
-
* until the request scope closes, including while the handler is running.
|
|
11
|
-
*/
|
|
12
|
-
const middleware = (service, options) => {
|
|
13
|
-
const errors = options.errors.map((schema) => ({
|
|
14
|
-
matches: Schema.is(schema),
|
|
15
|
-
encode: HttpServerResponse.schemaJson(schema),
|
|
16
|
-
status: SchemaAST.resolveAt("httpApiStatus")(schema.ast) ?? 500
|
|
17
|
-
}));
|
|
18
|
-
return HttpRouter.middleware()((httpEffect) => options.authenticate.pipe(Effect.matchEffect({
|
|
19
|
-
onFailure: (error) => {
|
|
20
|
-
const selected = errors.find((candidate) => candidate.matches(error));
|
|
21
|
-
if (selected === void 0) return Effect.die(/* @__PURE__ */ new Error("Undeclared authentication error"));
|
|
22
|
-
return selected.encode(error, {
|
|
23
|
-
status: selected.status,
|
|
24
|
-
...options.headers === void 0 ? {} : { headers: options.headers(error) }
|
|
25
|
-
}).pipe(Effect.orDie);
|
|
26
|
-
},
|
|
27
|
-
onSuccess: (identity) => Effect.provideService(httpEffect, service, identity)
|
|
28
|
-
}), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")))));
|
|
29
|
-
};
|
|
30
|
-
//#endregion
|
|
31
|
-
export { middleware, Authentication_exports as t };
|
package/dist/http.d.mts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { Any, Codec, SchemaErrorPolicy, t as Action_d_exports } from "./Action.mjs";
|
|
2
|
-
import { t as Implementation } from "./implementation-D75QAcD3.mjs";
|
|
3
|
-
import { Effect, Layer, Schema } from "effect";
|
|
4
|
-
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";
|
|
5
|
-
import { Etag, HttpClient, HttpPlatform, HttpRouter } from "effect/unstable/http";
|
|
6
|
-
import { FileSystem } from "effect/FileSystem";
|
|
7
|
-
import { Path } from "effect/Path";
|
|
8
|
-
declare namespace ActionHttp_d_exports {
|
|
9
|
-
export { Actions, Client, ClientOptions, LayerOptions, Options, api, client, configure, layer, openapi };
|
|
10
|
-
}
|
|
11
|
-
export interface Options<Errors extends ReadonlyArray<Codec> = []> {
|
|
12
|
-
readonly apiPath: `/${string}`;
|
|
13
|
-
readonly schemaError?: SchemaErrorPolicy<Errors>;
|
|
14
|
-
}
|
|
15
|
-
/** Layer/configure options: mount path plus OpenAPI document route. */
|
|
16
|
-
export interface LayerOptions<Errors extends ReadonlyArray<Codec> = []> extends Options<Errors> {
|
|
17
|
-
/** Set false when the host serves a combined document for multiple groups. */
|
|
18
|
-
readonly openapiPath: `/${string}` | false;
|
|
19
|
-
}
|
|
20
|
-
/** A group or its implementation. */
|
|
21
|
-
export interface Actions<A extends ReadonlyArray<Any> = ReadonlyArray<Any>> {
|
|
22
|
-
readonly actions: A;
|
|
23
|
-
}
|
|
24
|
-
/** Distribute over the tuple so each name retains its own codecs. */
|
|
25
|
-
type Endpoint<A extends Any, E extends Codec> = A extends {
|
|
26
|
-
readonly http: false;
|
|
27
|
-
} ? 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;
|
|
28
|
-
/** The native `HttpApi` for every HTTP-enabled action: `POST <apiPath>/<name>`. */
|
|
29
|
-
export declare function api<A extends ReadonlyArray<Any>, Errors extends ReadonlyArray<Codec> = []>(group: Actions<A>, options: Options<Errors>): HttpApi.HttpApi<"actions", HttpApiGroup.HttpApiGroup<"actions", Endpoint<A[number], Errors[number]>>>;
|
|
30
|
-
/** The OpenAPI 3.1 document Effect derives from `api`. */
|
|
31
|
-
export declare const openapi: <Errors extends ReadonlyArray<Codec> = []>(group: Actions, options: Options<Errors>) => OpenApi.OpenAPISpec;
|
|
32
|
-
/**
|
|
33
|
-
* Register the endpoints and optional OpenAPI document route on the router.
|
|
34
|
-
* Handler requirements are request-level requirements, exactly as native
|
|
35
|
-
* `HttpApiBuilder` handlers' are. Groups with no HTTP action register nothing.
|
|
36
|
-
*/
|
|
37
|
-
export declare const layer: <Actions extends ReadonlyArray<Any>, R, EX, RX, Errors extends ReadonlyArray<Codec> = []>(app: Implementation<Actions, R, EX, RX>, options: LayerOptions<Errors>) => Layer.Layer<never, EX, RX | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", R> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
|
|
38
|
-
type NativeClientOptions = NonNullable<Parameters<typeof HttpApiClient.make>[1]>;
|
|
39
|
-
/** Connection options plus the transport configuration used by HTTP clients. */
|
|
40
|
-
export type ClientOptions<Errors extends ReadonlyArray<Codec> = []> = NativeClientOptions & Options<Errors>;
|
|
41
|
-
/** Direct decoded-input methods, excluding MCP-only actions. */
|
|
42
|
-
export type Client<A extends ReadonlyArray<Any>, E extends Codec = never> = { readonly [Item in A[number] as Item["http"] extends false ? never : Item["name"]]: (...args: {} extends Item["input"]["Type"] ? [input?: Item["input"]["Type"]] : [input: Item["input"]["Type"]]) => HttpApiClient.Client.MethodReturn<Endpoint<Item, E>, never, never, "decoded-only">; };
|
|
43
|
-
/** Bind transport configuration once for contracts, routes, documents and clients. */
|
|
44
|
-
export declare const configure: <Errors extends ReadonlyArray<Codec> = []>(options: LayerOptions<Errors>) => {
|
|
45
|
-
api: <A extends ReadonlyArray<Any>>(group: Actions<A>) => HttpApi.HttpApi<"actions", HttpApiGroup.HttpApiGroup<"actions", Endpoint<A[number], Errors[number]>, false>>;
|
|
46
|
-
openapi: (group: Actions) => OpenApi.OpenAPISpec;
|
|
47
|
-
layer: <A extends ReadonlyArray<Any>, R, EX, RX>(app: Implementation<A, R, EX, RX>) => Layer.Layer<never, EX, RX | FileSystem | Etag.Generator | HttpPlatform.HttpPlatform | HttpRouter.HttpRouter | Path | HttpRouter.Request.From<"Requires", R>>;
|
|
48
|
-
client: <A extends ReadonlyArray<Any>>(group: Actions<A>, { baseUrl, transformClient, transformResponse }?: NativeClientOptions) => Effect.Effect<Client<A, Errors[number]>, never, HttpClient.HttpClient>;
|
|
49
|
-
};
|
|
50
|
-
/** Direct action methods backed by the native HTTP client and its codecs. */
|
|
51
|
-
export declare function client<A extends ReadonlyArray<Any>, Errors extends ReadonlyArray<Codec> = []>(group: Actions<A>, options: ClientOptions<Errors>): Effect.Effect<Client<A, Errors[number]>, never, HttpClient.HttpClient>;
|
|
52
|
-
//#endregion
|
|
53
|
-
export { ActionHttp_d_exports as t };
|
package/dist/http.mjs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import { n as handlerFor, t as Implementation } from "./implementation-BkLqMWtv.mjs";
|
|
3
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
|
-
import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
|
|
5
|
-
//#region src/ActionHttp.ts
|
|
6
|
-
var ActionHttp_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
-
api: () => api,
|
|
8
|
-
client: () => client,
|
|
9
|
-
configure: () => configure,
|
|
10
|
-
layer: () => layer,
|
|
11
|
-
openapi: () => openapi
|
|
12
|
-
});
|
|
13
|
-
const endpoint = (apiPath, action, errors) => HttpApiEndpoint.post(action.name, `${apiPath}/${action.name}`, {
|
|
14
|
-
payload: action.input,
|
|
15
|
-
success: action.success,
|
|
16
|
-
error: [...action.errors, ...errors]
|
|
17
|
-
}).annotate(OpenApi.Description, action.description);
|
|
18
|
-
function api(group, options) {
|
|
19
|
-
const [first, ...rest] = group.actions.filter((action) => action.http).map((action) => endpoint(options.apiPath, action, options.schemaError?.errors ?? []));
|
|
20
|
-
if (first === void 0) throw new Error("No HTTP-enabled actions");
|
|
21
|
-
return HttpApi.make("actions").add(HttpApiGroup.make("actions").add(first, ...rest));
|
|
22
|
-
}
|
|
23
|
-
/** The OpenAPI 3.1 document Effect derives from `api`. */
|
|
24
|
-
const openapi = (group, options) => OpenApi.fromApi(api(group, options));
|
|
25
|
-
/**
|
|
26
|
-
* Register the endpoints and optional OpenAPI document route on the router.
|
|
27
|
-
* Handler requirements are request-level requirements, exactly as native
|
|
28
|
-
* `HttpApiBuilder` handlers' are. Groups with no HTTP action register nothing.
|
|
29
|
-
*/
|
|
30
|
-
const layer = (app, options) => {
|
|
31
|
-
const exposed = app.actions.filter((action) => action.http);
|
|
32
|
-
if (exposed.length === 0) return Layer.empty;
|
|
33
|
-
const policy = options.schemaError;
|
|
34
|
-
class SchemaErrors extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: policy?.errors ?? [] }) {}
|
|
35
|
-
const schemaErrors = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy === void 0 ? failure : policy.map({
|
|
36
|
-
phase: failure.kind === "Body" || failure.kind === "ResponseHeaders" ? "output" : "input",
|
|
37
|
-
cause: failure.cause
|
|
38
|
-
})));
|
|
39
|
-
const httpApi = api(app, options).middleware(SchemaErrors);
|
|
40
|
-
return Implementation.register(app, (table) => {
|
|
41
|
-
const group = HttpApiBuilder.group(httpApi, "actions", (handlers) => handlers.handleAll(Object.fromEntries(exposed.map((action) => {
|
|
42
|
-
const handle = handlerFor(table, action);
|
|
43
|
-
return [action.name, (request) => handle(request.payload)];
|
|
44
|
-
}))));
|
|
45
|
-
const isolated = Layer.fromBuildMemo((memoMap, scope) => Layer.buildWithMemoMap(group.pipe(Layer.provide(schemaErrors)), memoMap, scope).pipe(Effect.setContext(Context.empty())));
|
|
46
|
-
return HttpApiBuilder.layer(httpApi, { openapiPath: options.openapiPath === false ? void 0 : options.openapiPath }).pipe(Layer.provide(isolated));
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
/** Bind transport configuration once for contracts, routes, documents and clients. */
|
|
50
|
-
const configure = (options) => ({
|
|
51
|
-
api: (group) => api(group, options),
|
|
52
|
-
openapi: (group) => openapi(group, options),
|
|
53
|
-
layer: (app) => layer(app, options),
|
|
54
|
-
client: (group, { baseUrl, transformClient, transformResponse } = {}) => client(group, {
|
|
55
|
-
...options,
|
|
56
|
-
baseUrl,
|
|
57
|
-
transformClient,
|
|
58
|
-
transformResponse
|
|
59
|
-
})
|
|
60
|
-
});
|
|
61
|
-
function client(group, options) {
|
|
62
|
-
return Effect.map(HttpApiClient.make(api(group, options), options), (native) => {
|
|
63
|
-
const acceptsUndefined = new Set(group.actions.filter((action) => Schema.is(action.input)(void 0)).map((action) => action.name));
|
|
64
|
-
return Object.fromEntries(Object.entries(native.actions).map(([name, method]) => {
|
|
65
|
-
if (name === "then") throw new Error("Action \"then\" requires the native grouped HttpApiClient");
|
|
66
|
-
return [name, (input) => method({
|
|
67
|
-
payload: input === void 0 && !acceptsUndefined.has(name) ? {} : input,
|
|
68
|
-
responseMode: "decoded-only"
|
|
69
|
-
})];
|
|
70
|
-
}));
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
//#endregion
|
|
74
|
-
export { api, client, configure, layer, openapi, ActionHttp_exports as t };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
//#region src/internal/implementation.ts
|
|
3
|
-
const handlerFor = (table, action) => {
|
|
4
|
-
const handle = table[action.name];
|
|
5
|
-
if (handle === void 0) throw new Error(`Missing handler: ${action.name}`);
|
|
6
|
-
return handle;
|
|
7
|
-
};
|
|
8
|
-
let implementations = 0;
|
|
9
|
-
/**
|
|
10
|
-
* Opaque implementation value; only its type is public. `R` is required per
|
|
11
|
-
* request, `EX`/`RX` describe handler acquisition at Layer build. Layer
|
|
12
|
-
* memoization lets both adapters share one handler build per runtime.
|
|
13
|
-
*/
|
|
14
|
-
var Implementation = class Implementation {
|
|
15
|
-
actions;
|
|
16
|
-
#handlers;
|
|
17
|
-
#layer;
|
|
18
|
-
constructor(actions, handlers, layer) {
|
|
19
|
-
this.actions = actions;
|
|
20
|
-
this.#handlers = handlers;
|
|
21
|
-
this.#layer = layer;
|
|
22
|
-
}
|
|
23
|
-
static make(actions, build) {
|
|
24
|
-
const handlers = Context.Service(`effect-actions/Handlers#${++implementations}`);
|
|
25
|
-
return new Implementation(actions, handlers, Layer.effect(handlers, build));
|
|
26
|
-
}
|
|
27
|
-
static register(app, make) {
|
|
28
|
-
return Layer.unwrap(Effect.map(app.#handlers, make)).pipe(Layer.provide(app.#layer));
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
//#endregion
|
|
32
|
-
export { handlerFor as n, Implementation as t };
|