@gjermundgaraba/effect-actions 0.1.0-rc.1 → 0.1.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 (49) hide show
  1. package/README.md +50 -219
  2. package/dist/Action.d.mts +62 -2
  3. package/dist/Action.mjs +7 -4
  4. package/dist/ActionCatalog.d.mts +31 -0
  5. package/dist/ActionCatalog.mjs +36 -0
  6. package/dist/ActionCli.d.mts +27 -0
  7. package/dist/ActionCli.mjs +26 -0
  8. package/dist/ActionCliClient.d.mts +45 -0
  9. package/dist/ActionCliClient.mjs +52 -0
  10. package/dist/ActionGroup.d.mts +15 -14
  11. package/dist/ActionGroup.mjs +3 -2
  12. package/dist/ActionHttp.d.mts +21 -34
  13. package/dist/ActionHttp.mjs +44 -51
  14. package/dist/ActionMcp.d.mts +28 -7
  15. package/dist/ActionMcp.mjs +20 -128
  16. package/dist/ActionToolkit.d.mts +37 -0
  17. package/dist/ActionToolkit.mjs +10 -0
  18. package/dist/Authentication.d.mts +11 -12
  19. package/dist/Authentication.mjs +24 -57
  20. package/dist/Testing.d.mts +7 -4
  21. package/dist/Testing.mjs +4 -2
  22. package/dist/TestingClient.d.mts +6 -3
  23. package/dist/TestingClient.mjs +2 -2
  24. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  25. package/dist/internal/actions.d.mts +18 -0
  26. package/dist/internal/actions.mjs +16 -0
  27. package/dist/internal/cli.d.mts +29 -0
  28. package/dist/internal/cli.mjs +36 -0
  29. package/dist/internal/implementation.d.mts +39 -0
  30. package/dist/internal/implementation.mjs +37 -0
  31. package/dist/internal/tools.mjs +84 -0
  32. package/docs/Action.md +117 -0
  33. package/docs/ActionCatalog.md +56 -0
  34. package/docs/ActionCli.md +91 -0
  35. package/docs/ActionCliClient.md +77 -0
  36. package/docs/ActionGroup.md +141 -0
  37. package/docs/ActionHttp.md +106 -0
  38. package/docs/ActionMcp.md +131 -0
  39. package/docs/ActionToolkit.md +73 -0
  40. package/docs/Authentication.md +114 -0
  41. package/docs/CONTEXT.md +36 -0
  42. package/docs/README.md +85 -0
  43. package/docs/Testing.md +91 -0
  44. package/docs/guarantees.md +55 -0
  45. package/docs/setup.md +63 -0
  46. package/package.json +19 -14
  47. package/dist/Action-CXsfTEhx.d.mts +0 -54
  48. package/dist/implementation-4Q2BESw5.mjs +0 -61
  49. package/dist/implementation-DTqp_0DW.d.mts +0 -61
@@ -1,29 +1,30 @@
1
- import { a as Handler, c as SchemaErrorPolicy, i as Codec, n as Action_d_exports, r as Any$1, t as Action } from "./Action-CXsfTEhx.mjs";
2
- import { i as Implementation, o as Actions } from "./implementation-DTqp_0DW.mjs";
1
+ import { Action, Action_d_exports, Any as Any$1, Codec, Handler } from "./Action.mjs";
2
+ import { Actions, SchemaErrorPolicy } from "./internal/actions.mjs";
3
+ import { Implementation } from "./internal/implementation.mjs";
3
4
  import { Effect, Scope } from "effect";
4
- //#region src/ActionGroup.d.ts
5
+ declare namespace ActionGroup_d_exports {
6
+ export { Any, Group, Implementation, Options, SchemaErrorPolicy, make };
7
+ }
5
8
  type HandlersFrom<Actions extends ReadonlyArray<Any$1>> = { readonly [A in Actions[number] as A["name"]]: Handler<A, any>; };
6
- type HandlersContext<H> = { [K in keyof H]: H[K] extends ((input: never) => Effect.Effect<infer _A, infer _E, infer R>) ? R : never; }[keyof H];
7
9
  /** Group-level errors join each action's own, so a shared set is declared once. */
8
- type WithErrors<Actions extends ReadonlyArray<Any$1>, Errors extends ReadonlyArray<Codec>> = { readonly [K in keyof Actions]: Actions[K] extends Action<infer Name, infer Input, infer Output, infer Own, infer Http> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Http> : never; };
10
+ type WithErrors<Actions extends ReadonlyArray<Any$1>, Errors extends ReadonlyArray<Codec>> = { readonly [K in keyof Actions]: Actions[K] extends Action<infer Name, infer Input, infer Output, infer Own, infer Http, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Http, Mcp> : never; };
11
+ /** What `make` needs to define a group. */
9
12
  export interface Options<Name extends string, Errors extends ReadonlyArray<Codec>, PolicyErrors extends ReadonlyArray<Codec>> {
10
13
  /** The `HttpApiGroup` identifier, and so the OpenAPI tag and operation-ID prefix. */
11
14
  readonly name: Name;
12
15
  /** Failures every action of the group may have, added to each action's own. */
13
16
  readonly errors?: Errors;
14
- /** How both transports answer failed decoding or encoding of these actions. */
17
+ /** How HTTP answers failed decoding or encoding of these actions; MCP keeps its native answers. */
15
18
  readonly schemaError?: SchemaErrorPolicy<PolicyErrors>;
16
19
  }
20
+ /** A named set of action contracts: what adapters serve and what `implement` binds. */
17
21
  export interface Group<Name extends string, Actions$1 extends ReadonlyArray<Any$1>, PolicyErrors extends ReadonlyArray<Codec> = ReadonlyArray<Codec>> extends Actions<Name, Actions$1, PolicyErrors> {
18
- /**
19
- * Bind every handler at once. Pass an Effect to resolve build-time services
20
- * once (`const users = yield* Users`); services yielded inside a handler are
21
- * request-scoped instead. Scoped acquisition lasts for the adapter runtime.
22
- */
23
- readonly implement: <H extends HandlersFrom<Actions$1>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Group<Name, Actions$1, PolicyErrors>, HandlersContext<H>, NoInfer<EX>, NoInfer<Exclude<RX, Scope.Scope>>>;
22
+ /** Bind every handler at once, resolving build-time services once per adapter layer. */
23
+ readonly implement: <H extends HandlersFrom<Actions$1>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Group<Name, Actions$1, PolicyErrors>, H, NoInfer<EX>, NoInfer<Exclude<RX, Scope.Scope>>>;
24
24
  }
25
+ /** Any group, with its actions erased. */
25
26
  export type Any = Group<string, ReadonlyArray<Any$1>>;
26
- /** Duplicate action and MCP names fail at definition time. */
27
+ /** Define a group. Invalid or duplicate action and MCP names fail here, at definition time. */
27
28
  export declare function make<const Name extends string, const Actions extends ReadonlyArray<Any$1>, const Errors extends ReadonlyArray<Codec> = [], const PolicyErrors extends ReadonlyArray<Codec> = []>(options: Options<Name, Errors, PolicyErrors>, ...actions: Actions): Group<Name, WithErrors<Actions, Errors>, PolicyErrors>;
28
29
  //#endregion
29
- export type { Implementation };
30
+ export { ActionGroup_d_exports, type Implementation, type SchemaErrorPolicy };
@@ -1,9 +1,10 @@
1
- import { n as assertDistinct, t as Implementation } from "./implementation-4Q2BESw5.mjs";
1
+ import { assertDistinct, assertName } from "./internal/actions.mjs";
2
+ import { Implementation } from "./internal/implementation.mjs";
2
3
  import { Effect } from "effect";
3
4
  //#region src/ActionGroup.ts
4
5
  function make(options, ...declared) {
5
6
  const { name } = options;
6
- if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) throw new Error(`Invalid action group name: ${name}`);
7
+ assertName("action group name", name);
7
8
  const actions = declared.map((action) => ({
8
9
  ...action,
9
10
  errors: [...action.errors, ...options.errors ?? []]
@@ -1,52 +1,39 @@
1
- import { d as __exportAll, i as Codec, n as Action_d_exports, r as Any } from "./Action-CXsfTEhx.mjs";
2
- import { a as RequestContext, n as BuildContext, o as Actions, r as BuildError, s as PolicyError, t as AnyImplementation } from "./implementation-DTqp_0DW.mjs";
3
- import { Effect, Layer, Schema } from "effect";
4
- import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
5
- import { Etag, HttpClient, HttpPlatform, HttpRouter } from "effect/unstable/http";
1
+ import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
+ import { Actions, PolicyError } from "./internal/actions.mjs";
3
+ import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
4
+ import { Layer, Schema } from "effect";
5
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
6
+ import { Etag, HttpPlatform, HttpRouter } from "effect/unstable/http";
6
7
  import { FileSystem } from "effect/FileSystem";
7
8
  import { Path } from "effect/Path";
8
9
  declare namespace ActionHttp_d_exports {
9
- export { Api, Client, ClientOptions, Http, Options, make };
10
+ export { Api, Http, Options, make };
10
11
  }
11
12
  /** Contract-level configuration: servers and clients must agree on it. */
12
13
  export interface Options {
13
14
  readonly apiPath: `/${string}`;
14
15
  }
15
- /** Distribute over the tuple so each name retains its own codecs. */
16
16
  type Endpoint<A extends Any, E extends Codec> = A extends {
17
17
  readonly http: false;
18
- } ? 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;
19
- /** One native group per action group; groups without HTTP actions are omitted. */
18
+ } ? never : A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["errors"][number] | E>, never> : never;
20
19
  type ApiGroup<G extends Actions> = G extends Actions ? [Endpoint<G["actions"][number], never>] extends [never] ? never : HttpApiGroup.HttpApiGroup<G["name"], Endpoint<G["actions"][number], PolicyError<G>>> : never;
20
+ /** The native `HttpApi` of every HTTP-enabled action of `G`, one group per action group. */
21
21
  export type Api<G extends Actions> = HttpApi.HttpApi<"actions", ApiGroup<G>>;
22
- type Served<G extends Actions> = G extends Actions ? G["actions"][number] extends (infer Item) ? Item extends Any ? {
23
- readonly action: Item;
24
- readonly policyError: PolicyError<G>;
25
- } : never : never : never;
26
- /** Direct decoded-input methods of every group, excluding MCP-only actions. */
27
- export type Client<G extends Actions> = { readonly [Pair in Served<G> as Pair["action"]["http"] extends false ? never : Pair["action"]["name"]]: (...args: {} extends Pair["action"]["input"]["Type"] ? [input?: Pair["action"]["input"]["Type"]] : [input: Pair["action"]["input"]["Type"]]) => HttpApiClient.Client.MethodReturn<Endpoint<Pair["action"], Pair["policyError"]>, never, never, "decoded-only">; };
28
- /** Native connection options: `baseUrl`, `transformClient`, `transformResponse`. */
29
- export type ClientOptions = NonNullable<Parameters<typeof HttpApiClient.make>[1]>;
30
- export interface Http<G extends Actions> {
22
+ /** A namespaced HTTP binding. */
23
+ export interface Http<Groups extends ReadonlyArray<Actions>> {
24
+ /** The exact contracts bound to this HTTP adapter, in declaration order. */
25
+ readonly groups: Groups;
26
+ readonly api: Api<Groups[number]>;
31
27
  /**
32
- * The native `HttpApi` for every HTTP-enabled action: `POST <apiPath>/<name>`.
33
- * Documents, documentation UIs and native clients are Effect's own, from this value.
28
+ * Serve any number of implementations in one layer. Build failures, build
29
+ * services and request services are unions over precisely those apps.
34
30
  */
35
- readonly api: Api<G>;
36
- /**
37
- * The routes of one group. Merge one per group; router middleware provided
38
- * to a layer applies to that group only. Handler requirements are
39
- * request-level requirements, exactly as native `HttpApiBuilder` handlers'
40
- * are. A group without HTTP actions registers nothing and is not acquired.
41
- */
42
- readonly layer: <App extends AnyImplementation<G>>(app: App) => Layer.Layer<never, BuildError<App>, BuildContext<App> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<App>> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
43
- /** Direct action methods backed by the native HTTP client and its codecs. */
44
- readonly client: (options?: ClientOptions) => Effect.Effect<Client<G>, never, HttpClient.HttpClient>;
31
+ readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>>(...apps: Apps) => Layer.Layer<never, BuildError<Apps[number]>, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
45
32
  }
46
33
  /**
47
- * Bind the contract-level configuration once. The native API is built here and
48
- * shared by the routes and clients, so they cannot disagree.
34
+ * Bind the contract-level configuration once. Every route is `POST
35
+ * <apiPath>/<group>/<action>`, so action names may repeat across groups.
49
36
  */
50
- export declare function make<const G extends ReadonlyArray<Actions>>(options: Options, ...groups: G): Http<G[number]>;
37
+ export declare function make<const G extends ReadonlyArray<Actions>>(options: Options, ...groups: G): Http<G>;
51
38
  //#endregion
52
- export { ActionHttp_d_exports as t };
39
+ export { ActionHttp_d_exports };
@@ -1,73 +1,66 @@
1
- import { n as assertDistinct, r as served, t as Implementation } from "./implementation-4Q2BESw5.mjs";
2
- import { Context, Effect, Layer, Schema } from "effect";
3
- import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
1
+ import { assertDistinct } from "./internal/actions.mjs";
2
+ import { dispatch } from "./internal/implementation.mjs";
3
+ import { Effect, Layer } from "effect";
4
+ import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
4
5
  //#region src/ActionHttp.ts
5
- const endpoint = (apiPath, action, errors) => HttpApiEndpoint.post(action.name, `${apiPath}/${action.name}`, {
6
+ const httpActions = (group) => group.actions.filter((action) => action.http);
7
+ const endpoint = (apiPath, group, action) => HttpApiEndpoint.post(action.name, `${apiPath}/${group.name}/${action.name}`, {
6
8
  payload: action.input,
7
9
  success: action.success,
8
- error: [...action.errors, ...errors]
10
+ error: [...action.errors, ...group.schemaError?.errors ?? []]
9
11
  }).annotate(OpenApi.Description, action.description);
10
- function erasedApi(views, options) {
11
- const [first, ...rest] = views.flatMap(({ group, actions }) => {
12
- const [head, ...tail] = actions.map((action) => endpoint(options.apiPath, action, group.schemaError?.errors ?? []));
13
- return head === void 0 ? [] : [HttpApiGroup.make(group.name).add(head, ...tail)];
12
+ function erasedApi(groups, options) {
13
+ const [first, ...rest] = groups.flatMap((group) => {
14
+ const [first, ...rest] = httpActions(group).map((action) => endpoint(options.apiPath, group, action));
15
+ return first === void 0 ? [] : [HttpApiGroup.make(group.name).add(first, ...rest)];
14
16
  });
15
17
  const empty = HttpApi.make("actions");
16
18
  return first === void 0 ? empty : empty.add(first, ...rest);
17
19
  }
18
20
  const schemaErrorMiddleware = (policy) => {
19
- class SchemaErrors extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: policy?.errors ?? [] }) {}
21
+ class SchemaErrors extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: policy.errors }) {}
20
22
  return {
21
23
  SchemaErrors,
22
- layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy === void 0 ? failure : policy.map({
23
- phase: failure.kind === "Body" || failure.kind === "ResponseHeaders" ? "output" : "input",
24
- cause: failure.cause
25
- })))
24
+ layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy.map(failure)))
26
25
  };
27
26
  };
28
- /** One group registers its own routes, so middleware provided to this layer is its alone. */
29
- const erasedLayer = (binding, app) => {
30
- if (!binding.groups.includes(app.group)) throw new Error(`Implementation of group "${app.group.name}" is not served by this adapter`);
31
- const view = binding.views.find(({ group }) => group === app.group);
32
- if (view === void 0) return Layer.empty;
33
- const schemaErrors = schemaErrorMiddleware(view.group.schemaError);
34
- const httpApi = erasedApi([view], binding.options).middleware(schemaErrors.SchemaErrors);
35
- return Implementation.register([{
36
- app,
37
- actions: view.actions
38
- }], (bound) => {
39
- const handlers = bound.map((implementation) => HttpApiBuilder.group(httpApi, implementation.group.name, (builder) => builder.handleAll(Object.fromEntries(implementation.actions.map((action) => {
40
- const handle = implementation.handle(action);
41
- return [action.name, (request) => handle(request.payload)];
42
- })))));
43
- const isolated = Layer.fromBuildMemo((memoMap, scope) => Layer.buildWithMemoMap(Layer.mergeAll(Layer.empty, ...handlers).pipe(Layer.provide(schemaErrors.layer)), memoMap, scope).pipe(Effect.setContext(Context.empty())));
44
- return HttpApiBuilder.layer(httpApi).pipe(Layer.provide(isolated));
27
+ /**
28
+ * `HttpApiBuilder.handleAll` registers a dynamically assembled record. The
29
+ * action selected by the native router determines the input before `dispatch`
30
+ * calls its handler, so this is the one adapter boundary where exact handlers
31
+ * are viewed as an erased record. `HandlersContext` remains on the dispatch
32
+ * effect and is restored by `Http.layer`'s public signature.
33
+ */
34
+ const erasedHandlers = (handlers) => {
35
+ return handlers;
36
+ };
37
+ /** Build routes for one group. Handler erasure is limited to native dynamic endpoint registration. */
38
+ const groupLayer = (options, app) => {
39
+ const actions = httpActions(app.group);
40
+ if (actions.length === 0) return Layer.empty;
41
+ const api = erasedApi([app.group], options);
42
+ const entries = (record) => Object.fromEntries(actions.map((action) => [action.name, (request) => dispatch(app.group, action, record)(request.payload)]));
43
+ const handlers = (record) => HttpApiBuilder.group(api, app.group.name, (builder) => builder.handleAll(entries(record)));
44
+ if (app.group.schemaError === void 0) return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.layer(api).pipe(Layer.provide(handlers(erasedHandlers(record))))));
45
+ const schemaErrors = schemaErrorMiddleware(app.group.schemaError);
46
+ const policyApi = api.middleware(schemaErrors.SchemaErrors);
47
+ const policyHandlers = (record) => HttpApiBuilder.group(policyApi, app.group.name, (builder) => {
48
+ const dynamicEntries = entries(record);
49
+ return builder.handleAll(dynamicEntries);
45
50
  });
51
+ return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.layer(policyApi).pipe(Layer.provide(policyHandlers(erasedHandlers(record))), Layer.provide(schemaErrors.layer))));
46
52
  };
47
- const erasedClient = (api, views, connection) => Effect.map(HttpApiClient.make(api, connection), (native) => {
48
- const acceptsUndefined = new Set(views.flatMap(({ actions }) => actions.flatMap((action) => Schema.is(action.input)(void 0) ? [action.name] : [])));
49
- return Object.fromEntries(Object.values(native).flatMap((methods) => Object.entries(methods).map(([name, method]) => {
50
- if (name === "then") throw new Error("Action \"then\" requires the native grouped HttpApiClient");
51
- return [name, (input) => method({
52
- payload: input === void 0 && !acceptsUndefined.has(name) ? {} : input,
53
- responseMode: "decoded-only"
54
- })];
55
- })));
56
- });
57
53
  function make(options, ...groups) {
58
- const views = served(groups, (action) => action.http);
59
54
  assertDistinct("action group", groups.map((group) => group.name));
60
- assertDistinct("action", views.flatMap(({ actions }) => actions.map((action) => action.name)));
61
- const api = erasedApi(views, options);
62
- const binding = {
63
- groups,
64
- views,
65
- options
55
+ const layer = (...apps) => {
56
+ assertDistinct("implementation group", apps.map((app) => app.group.name));
57
+ for (const app of apps) if (!groups.includes(app.group)) throw new Error(`Implementation of group "${app.group.name}" is not served by this adapter`);
58
+ return apps.map((app) => groupLayer(options, app)).reduce((all, app) => Layer.merge(all, app), Layer.empty);
66
59
  };
67
60
  return {
68
- api,
69
- layer: (app) => erasedLayer(binding, app),
70
- client: (connection) => erasedClient(api, views, connection)
61
+ groups,
62
+ api: erasedApi(groups, options),
63
+ layer
71
64
  };
72
65
  }
73
66
  //#endregion
@@ -1,20 +1,41 @@
1
- import { a as RequestContext, n as BuildContext, r as BuildError, t as AnyImplementation } from "./implementation-DTqp_0DW.mjs";
1
+ import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
2
2
  import { Cause, Layer } from "effect";
3
- import { McpProtocol } from "effect/unstable/ai";
3
+ import { McpServer } from "effect/unstable/ai";
4
4
  import { HttpRouter } from "effect/unstable/http";
5
- import { NonEmptyReadonlyArray } from "effect/Array";
5
+ import { Stdio } from "effect/Stdio";
6
6
  //#region src/ActionMcp.d.ts
7
+ /** One Streamable HTTP MCP endpoint; its fields become the native server info. */
7
8
  export interface Options {
8
9
  readonly name: string;
9
10
  readonly version: string;
11
+ /** Native protocol adapters to serve; negotiation and sessions are owned by Effect. */
12
+ readonly protocols: Parameters<typeof McpServer.layerHttp>[0]["protocols"];
13
+ /** Route of the Streamable HTTP endpoint; no default. */
10
14
  readonly path: HttpRouter.PathInput;
11
- readonly protocols?: NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>;
15
+ /** Browser origins accepted by the native server; passed through to `McpServer.layerHttp`. */
12
16
  readonly allowedOrigins?: ReadonlyArray<string>;
13
17
  readonly instructions?: string;
14
18
  }
19
+ /** Server information for an MCP subprocess connected through standard I/O. */
20
+ export interface StdioOptions {
21
+ readonly name: string;
22
+ readonly version: string;
23
+ /** Native protocol adapters to serve; negotiation is owned by Effect. */
24
+ readonly protocols: Parameters<typeof McpServer.layerStdio>[0]["protocols"];
25
+ readonly instructions?: string;
26
+ }
27
+ /**
28
+ * Serve MCP tools over one Streamable HTTP endpoint.
29
+ *
30
+ * Middleware provided around this layer has the normal HTTP lifetime. Native
31
+ * context capture applies: never provide request-identity tags at startup.
32
+ */
33
+ export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>>(options: Options, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>>>;
15
34
  /**
16
- * One Streamable HTTP MCP endpoint serving every MCP-enabled action of `apps`.
17
- * An endpoint is one route, so middleware provided to this layer covers all of its tools.
35
+ * Serve MCP tools through newline-delimited JSON-RPC on standard I/O.
36
+ *
37
+ * The host supplies the `Stdio` service. Arguments are tool input only and
38
+ * never establish request identity or authority.
18
39
  */
19
- export declare function layer<const Apps extends ReadonlyArray<AnyImplementation>>(options: Options, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>>>;
40
+ export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>>(options: StdioOptions, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | Stdio | RequestContext<Apps[number]>>;
20
41
  //#endregion
@@ -1,133 +1,25 @@
1
- import { n as assertDistinct, r as served, t as Implementation } from "./implementation-4Q2BESw5.mjs";
2
- import { Context, Effect, JsonPointer, Layer, Predicate, Schema } from "effect";
3
- import { McpProtocol, McpSchema, McpServer, Tool } from "effect/unstable/ai";
4
- import { HttpRouter } from "effect/unstable/http";
1
+ import "./internal/implementation.mjs";
2
+ import { bindTools } from "./internal/tools.mjs";
3
+ import { Layer } from "effect";
4
+ import { McpServer } from "effect/unstable/ai";
5
5
  //#region src/ActionMcp.ts
6
- /** Protocol revisions served over Streamable HTTP by default; 2026-07-28 is stateless. */
7
- const protocols = [
8
- McpProtocol.v2026_07_28,
9
- McpProtocol.v2025_11_25,
10
- McpProtocol.v2025_06_18,
11
- McpProtocol.v2025_03_26,
12
- McpProtocol.v2024_11_05
13
- ];
14
- const toolResult = (structuredContent, isError) => new McpSchema.CallToolResult({
15
- isError,
16
- structuredContent,
17
- content: [{
18
- type: "text",
19
- text: JSON.stringify(structuredContent)
20
- }]
21
- });
22
- const resolveReference = (name, what, schema, definitions) => {
23
- if (!Predicate.isString(schema.$ref)) return schema;
24
- const reference = schema.$ref;
25
- const path = JsonPointer.parseUriFragment(reference);
26
- const key = path?.[1];
27
- if (path?.length !== 2 || path[0] !== "$defs" || key === void 0) throw new Error(`${name}: unsupported MCP ${what} reference ${reference}`);
28
- const definition = Object.hasOwn(definitions, key) ? definitions[key] : void 0;
29
- if (definition === void 0) throw new Error(`${name}: missing MCP ${what} reference ${reference}`);
30
- return definition;
6
+ const registration = (apps) => {
7
+ const binding = bindTools(apps, "mcp");
8
+ return Layer.effectDiscard(McpServer.registerToolkit(binding.toolkit)).pipe(Layer.provide(binding.layer));
31
9
  };
32
- /** MCP `inputSchema` requires a literal object root. */
33
- const inputJsonSchema = (action) => {
34
- const { schema, definitions } = Schema.toJsonSchemaDocument(action.input);
35
- const root = resolveReference(action.name, "input", schema, definitions);
36
- if (root.type !== "object") throw new Error(`${action.name}: MCP input must have an object root; omit input for no arguments`);
37
- return Object.keys(definitions).length === 0 ? root : {
38
- ...root,
39
- $defs: definitions
40
- };
41
- };
42
- /** Declared errors are published as-is in `structuredContent`, so each must encode to an object. */
43
- const assertObjectError = (owner, error) => {
44
- const { schema, definitions } = Schema.toJsonSchemaDocument(error);
45
- const visited = /* @__PURE__ */ new Set();
46
- const encodesObject = (candidate) => {
47
- const root = resolveReference(owner, "error", candidate, definitions);
48
- if (root.type === "object" || visited.has(root)) return true;
49
- visited.add(root);
50
- const members = root.anyOf ?? root.oneOf;
51
- return Array.isArray(members) && members.length > 0 && members.every(encodesObject);
52
- };
53
- if (!encodesObject(schema)) throw new Error(`${owner}: MCP error must have an object root`);
54
- };
55
- const registerTool = (app, action, policy) => Effect.gen(function* () {
56
- if (action.mcp === false) return;
57
- const server = yield* McpServer.McpServer;
58
- const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(inputJsonSchema(action)).pipe(Effect.orDie);
59
- const errors = [...action.errors, ...policy?.errors ?? []];
60
- for (const error of action.errors) assertObjectError(action.name, error);
61
- const input = Schema.toCodecJson(action.input);
62
- const success = Schema.toCodecJson(action.success);
63
- const failure = Schema.toCodecJson(Schema.Union(errors));
64
- const handle = app.handle(action);
65
- const failureResult = (error) => Schema.encodeUnknownEffect(failure)(error).pipe(Effect.orDie, Effect.map((json) => toolResult(json, true)));
66
- const schemaFailure = (phase, cause) => {
67
- if (policy !== void 0) return failureResult(policy.map({
68
- phase,
69
- cause
70
- }));
71
- if (phase === "input") return Effect.fail(new McpSchema.InvalidParams({ message: cause.message }));
72
- return Effect.die(cause);
73
- };
74
- const successResult = (value) => Schema.encodeUnknownEffect(success)(value).pipe(Effect.matchEffect({
75
- onFailure: (cause) => schemaFailure("output", cause),
76
- onSuccess: (json) => Effect.succeed(toolResult({ value: json }, false))
10
+ const server = (apps, transport) => registration(apps).pipe(Layer.provide(transport), Layer.fresh);
11
+ function layerHttp(options, ...apps) {
12
+ return server(apps, McpServer.layerHttp({
13
+ name: options.name,
14
+ version: options.version,
15
+ instructions: options.instructions,
16
+ path: options.path,
17
+ protocols: options.protocols,
18
+ allowedOrigins: options.allowedOrigins
77
19
  }));
78
- yield* server.addTool({
79
- tool: new McpSchema.Tool({
80
- name: action.mcp.name,
81
- description: action.description,
82
- inputSchema,
83
- outputSchema: Tool.getJsonSchemaFromSchema(Schema.Struct({ value: action.success })),
84
- annotations: {
85
- readOnlyHint: action.mcp.readOnly,
86
- destructiveHint: action.mcp.destructive
87
- }
88
- }),
89
- annotations: Context.empty(),
90
- handle: (payload) => Schema.decodeUnknownEffect(input)(payload).pipe(Effect.matchEffect({
91
- onFailure: (cause) => schemaFailure("input", cause),
92
- onSuccess: (decoded) => handle(decoded).pipe(Effect.matchEffect({
93
- onSuccess: successResult,
94
- onFailure: failureResult
95
- }))
96
- }))
97
- });
98
- });
99
- const registrationRouter = (router, context) => HttpRouter.HttpRouter.of({
100
- ...router,
101
- add: (method, path, handler, options) => router.add(method, path, handler, options).pipe(Effect.provideContext(context)),
102
- addAll: (routes) => router.addAll(routes).pipe(Effect.provideContext(context)),
103
- prefixed: (prefix) => registrationRouter(router.prefixed(prefix), context)
104
- });
105
- const erasedLayer = (apps, options) => {
106
- const serving = apps.flatMap((app) => served([app.group], (action) => action.mcp !== false).map(({ actions }) => ({
107
- app,
108
- actions
109
- })));
110
- assertDistinct("MCP tool", serving.flatMap(({ actions }) => actions.flatMap((action) => action.mcp === false ? [] : [action.mcp.name])));
111
- return Implementation.register(serving, (bound) => {
112
- const native = Layer.effectDiscard(Effect.gen(function* () {
113
- for (const app of bound) for (const error of app.group.schemaError?.errors ?? []) assertObjectError(`Schema-error policy of group ${app.group.name}`, error);
114
- yield* Effect.forEach(bound.flatMap((app) => app.actions.map((action) => [app, action])), ([app, action]) => registerTool(app, action, app.group.schemaError), { discard: true });
115
- })).pipe(Layer.provide(McpServer.layerHttp({
116
- name: options.name,
117
- version: options.version,
118
- instructions: options.instructions,
119
- path: options.path,
120
- protocols: options.protocols ?? protocols,
121
- allowedOrigins: options.allowedOrigins
122
- })), Layer.fresh);
123
- return Layer.fromBuildMemo((memoMap, scope) => Effect.gen(function* () {
124
- const router = registrationRouter(yield* HttpRouter.HttpRouter, yield* Effect.context());
125
- return yield* Layer.buildWithMemoMap(native, memoMap, scope).pipe(Effect.setContext(Context.make(HttpRouter.HttpRouter, router)));
126
- }));
127
- });
128
- };
129
- function layer(options, ...apps) {
130
- return erasedLayer(apps, options);
20
+ }
21
+ function layerStdio(options, ...apps) {
22
+ return server(apps, McpServer.layerStdio(options));
131
23
  }
132
24
  //#endregion
133
- export { layer };
25
+ export { layerHttp, layerStdio };
@@ -0,0 +1,37 @@
1
+ import { Action_d_exports, Any } from "./Action.mjs";
2
+ import { AnyImplementation, BuildContext, BuildError, HandlerContext, Implementation } from "./internal/implementation.mjs";
3
+ import { Layer, Schema } from "effect";
4
+ import { Tool, Toolkit } from "effect/unstable/ai";
5
+ //#region src/ActionToolkit.d.ts
6
+ /** A native tool corresponding to an MCP-enabled action. */
7
+ type NativeTool<A extends Any, R> = A extends {
8
+ readonly mcp: {
9
+ readonly name: infer Name extends string;
10
+ };
11
+ } ? Tool.Tool<Name, {
12
+ readonly parameters: A["input"];
13
+ readonly success: A["success"];
14
+ readonly failure: Schema.Union<A["errors"]>;
15
+ readonly failureMode: "return";
16
+ }, R> : never;
17
+ type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
18
+ type ToolForAction<App extends AnyImplementation, A extends Any> = A extends {
19
+ readonly mcp: object;
20
+ } ? NativeTool<A, HandlerContext<HandlersOf<App>, Extract<A["name"], keyof HandlersOf<App>>>> : never;
21
+ type ToolsFor<App extends AnyImplementation> = App extends AnyImplementation ? ToolForAction<App, App["group"]["actions"][number]> : never;
22
+ type ToolkitTools<Apps extends ReadonlyArray<AnyImplementation>> = { readonly [T in ToolsFor<Apps[number]> as T["name"]]: T; };
23
+ /** Native tools and the layer that binds their action implementations. */
24
+ export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R = never> {
25
+ readonly toolkit: Toolkit.Toolkit<Tools>;
26
+ /** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
27
+ readonly layer: Layer.Layer<Tool.HandlersFor<Tools>, E, R>;
28
+ }
29
+ /**
30
+ * Project implementations into Effect's native AI toolkit.
31
+ *
32
+ * Unlike MCP, calls return the action's native success/failure values directly.
33
+ * Build services are needed to construct `layer`; request services are needed
34
+ * when the resulting toolkit handles a call.
35
+ */
36
+ export declare function make<const Apps extends ReadonlyArray<AnyImplementation>>(...apps: Apps): Binding<ToolkitTools<Apps>, BuildError<Apps[number]>, BuildContext<Apps[number]>>;
37
+ //#endregion
@@ -0,0 +1,10 @@
1
+ import "./internal/implementation.mjs";
2
+ import { bindTools } from "./internal/tools.mjs";
3
+ import "effect";
4
+ import "effect/unstable/ai";
5
+ //#region src/ActionToolkit.ts
6
+ function make(...apps) {
7
+ return bindTools(apps, "native");
8
+ }
9
+ //#endregion
10
+ export { make };
@@ -1,20 +1,16 @@
1
- import { i as Codec, n as Action_d_exports } from "./Action-CXsfTEhx.mjs";
2
1
  import { Context, Effect } from "effect";
3
- import { Headers, HttpRouter } from "effect/unstable/http";
2
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
4
3
  import { NonEmptyReadonlyArray } from "effect/Array";
5
4
  //#region src/Authentication.d.ts
6
- export interface Options<A, Errors extends ReadonlyArray<Codec>, R> {
7
- readonly authenticate: Effect.Effect<A, Errors[number]["Type"], R>;
8
- readonly errors: Errors;
9
- readonly headers?: (error: Errors[number]["Type"]) => Headers.Input;
10
- }
11
5
  /**
12
6
  * Authenticate each request and provide its identity to the downstream handler.
13
- * Only authentication failures are encoded, using each schema's httpApiStatus.
14
- * Dependencies remain native router request requirements. Acquired resources live
15
- * until the request scope closes, including while the handler is running.
7
+ * `authenticate` fails with the response to send instead, so the host owns its
8
+ * status, body and challenge headers. Dependencies remain native router request
9
+ * requirements. Acquired resources live until the request scope closes, including
10
+ * while the handler is running. Every response is marked `Cache-Control: no-store`,
11
+ * including private failures serialized by enclosing middleware.
16
12
  */
17
- 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<{
13
+ export declare const middleware: <I, A, R>(service: Context.Key<I, A>, authenticate: Effect.Effect<NoInfer<A>, HttpServerResponse.HttpServerResponse, R>) => HttpRouter.Middleware<{
18
14
  provides: I;
19
15
  handles: never;
20
16
  error: never;
@@ -22,13 +18,15 @@ export declare const middleware: <I, A, const Errors extends ReadonlyArray<Codec
22
18
  layerError: never;
23
19
  layerRequires: never;
24
20
  }>;
21
+ /** RFC 9728 metadata to publish; the host is responsible for these being valid OAuth URLs. */
25
22
  export interface ProtectedResourceOptions {
26
- /** Exact OAuth resource identifier. HTTPS, or HTTP on loopback for development. */
23
+ /** Exact OAuth resource identifier; its path and query select the discovery path. */
27
24
  readonly resource: string;
28
25
  readonly authorizationServers: NonEmptyReadonlyArray<string>;
29
26
  readonly scopesSupported?: ReadonlyArray<string>;
30
27
  readonly resourceName?: string;
31
28
  }
29
+ /** Parameters of one `WWW-Authenticate: Bearer` challenge, RFC 6750 §3. */
32
30
  export interface BearerChallengeOptions {
33
31
  readonly error?: "invalid_token" | "insufficient_scope";
34
32
  readonly errorDescription?: string;
@@ -42,6 +40,7 @@ export interface BearerChallengeOptions {
42
40
  export declare const protectedResource: (options: ProtectedResourceOptions) => {
43
41
  layer: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter>;
44
42
  metadataUrl: string;
43
+ /** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
45
44
  challenge: (challenge?: BearerChallengeOptions) => string;
46
45
  };
47
46
  //#endregion