@gjermundgaraba/effect-actions 0.1.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gjermund Garaba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @gjermundgaraba/effect-actions
2
+
3
+ Define an Effect action once, implement it once, expose it over **HTTP and MCP**.
4
+
5
+ **Experimental release candidate.** The repository currently uses Effect snapshot
6
+ [`4e4a3a6`](https://pkg.pr.new/Effect-TS/effect/effect@4e4a3a6). The MCP adapter
7
+ requires APIs missing from published `4.0.0-rc.115`, despite that being the current
8
+ peer declaration. Until these versions are aligned, use the pinned snapshot;
9
+ the declared peer version alone is not sufficient.
10
+
11
+ ## Quickstart
12
+
13
+ ```ts
14
+ import { Effect, Layer, Schema } from "effect";
15
+ import { Action, ActionGroup, ActionHttp, ActionMcp } from "@gjermundgaraba/effect-actions";
16
+
17
+ const Greet = Action.make("greet", {
18
+ description: "Greet someone by name.",
19
+ input: Schema.Struct({ name: Schema.String }),
20
+ success: Schema.String,
21
+ mcp: { readOnly: true },
22
+ });
23
+
24
+ export const Actions = ActionGroup.make(Greet);
25
+ const app = Actions.implement({
26
+ greet: ({ name }) => Effect.succeed(`Hello, ${name}!`),
27
+ });
28
+
29
+ export const routes = Layer.mergeAll(
30
+ ActionHttp.layer(app, {
31
+ apiPath: "/api/actions",
32
+ openapiPath: "/openapi.json",
33
+ }),
34
+ ActionMcp.layer(app, { name: "greetings", version: "1.0.0", path: "/mcp" }),
35
+ );
36
+ ```
37
+
38
+ Serve `routes` with Effect's `HttpRouter`. This creates `POST /api/actions/greet`,
39
+ `GET /openapi.json`, and an MCP endpoint at `/mcp`. The HTTP response is
40
+ `"Hello, Ada!"`; MCP returns `structuredContent: { value: "Hello, Ada!" }`.
41
+ See [examples/server.ts](examples/server.ts) for Node server wiring and the
42
+ [authenticated demo](examples/README.md) for a runnable application.
43
+
44
+ ### Contracts
45
+
46
+ - `input` defaults to `Action.NoInput` for actions without arguments.
47
+ - `error` is a list of failure schemas, defaulting to none. Each schema retains
48
+ its own `httpApiStatus` annotation.
49
+ - Actions default to both transports; use `http: false` or `mcp: false` to opt out.
50
+ - MCP input must have an object-root JSON Schema; declared errors must encode to
51
+ objects. The MCP adapter checks this at Layer construction. HTTP allows scalars.
52
+ - `mcp.name` overrides the tool name. `destructive` defaults to `!readOnly`.
53
+ - Schemas must be service-free. Handlers may require services.
54
+
55
+ ## HTTP client
56
+
57
+ Save the quickstart above as `quickstart.ts`. Once its routes are served:
58
+
59
+ ```ts
60
+ import { Effect } from "effect";
61
+ import { FetchHttpClient } from "effect/unstable/http";
62
+ import { ActionHttp } from "@gjermundgaraba/effect-actions";
63
+ import { Actions } from "./quickstart.js";
64
+
65
+ export const greeting = Effect.gen(function* () {
66
+ const client = yield* ActionHttp.client(Actions, {
67
+ apiPath: "/api/actions",
68
+ baseUrl: "http://127.0.0.1:3000",
69
+ });
70
+ return yield* client.greet({ name: "Ada" });
71
+ }).pipe(Effect.provide(FetchHttpClient.layer));
72
+ ```
73
+
74
+ Run `greeting` with `Effect.runPromise`. Methods take decoded inputs and return
75
+ decoded results; MCP-only actions are excluded. Authentication
76
+ headers can be added with `transformClient`. See [client details](docs/behavior.md#http-client-details)
77
+ for error types, optional inputs, and native grouped clients.
78
+
79
+ ## Adapter options
80
+
81
+ | API | Options |
82
+ | ---------------------------- | --------------------------------------------------------------------------------------- |
83
+ | `ActionHttp.layer` | `apiPath`, `openapiPath`, `schemaError` |
84
+ | `ActionHttp.api` / `openapi` | `apiPath`, `schemaError` |
85
+ | `ActionHttp.client` | `apiPath`, `schemaError`, `baseUrl`, `transformClient`, `transformResponse` |
86
+ | `ActionMcp.layer` | `name`, `version`, `path`, `protocols`, `allowedOrigins`, `instructions`, `schemaError` |
87
+
88
+ `apiPath`, `openapiPath` (on `layer` / `configure`), and MCP `path` are required —
89
+ the library has no defaults. `ActionHttp.configure(options)` binds HTTP configuration
90
+ for its `layer`, `api`, `openapi`, and `client` methods. Configured clients accept
91
+ only connection options. Set `openapiPath: false` when the host serves a combined
92
+ document.
93
+
94
+ See [adapter behavior](docs/behavior.md) for shared schema-error policies,
95
+ dependency lifetimes, wire formats, and MCP protocol support.
96
+
97
+ ## Scope
98
+
99
+ HTTP means JSON POST endpoints, not Effect's RPC wire protocol. Both adapters use
100
+ Effect's servers; there is no MCP SDK runtime dependency. Actions are unary:
101
+ no streaming, uploads, prompts, resources, or retries.
102
+
103
+ Authentication and authorization belong to the application. Tool discovery is
104
+ not filtered by actor. The example's bearer tokens are **demo-only**; production
105
+ needs real token validation, MCP authorization discovery, request limits, and
106
+ error reporting.
107
+
108
+ ## Development and packaging
109
+
110
+ Requires a supported Node.js version (24 LTS recommended) and [Vite+](https://viteplus.dev).
111
+
112
+ ```sh
113
+ vp install
114
+ vp check
115
+ vp test
116
+ vp run test:package
117
+ vp run example
118
+ ```
119
+
120
+ `vp run dev` watches the library build. `vp check` also verifies the compile-time
121
+ assertions in `tests/types.ts`. Tests cover both transports, official MCP clients,
122
+ context isolation, schema-error policies, and cancellation.
123
+
124
+ `vp run build` emits ESM and declarations into `dist/`. Public entry points are the
125
+ package root, `/Action`, `/ActionGroup`, `/http`, `/mcp`, `/authentication`, `/testing`, and `/testing/client`.
126
+ `vp run test:package` builds and checks a tarball in an isolated consumer using the pinned
127
+ Effect snapshot; it does not establish compatibility with the published peer.
128
+
129
+ ## Authentication and OAuth discovery
130
+
131
+ `Authentication.middleware(CurrentActor, { authenticate, errors, headers })` provides
132
+ an application-defined context service for each request. `authenticate` is an Effect;
133
+ `errors` lists its error schemas in precedence order. The first matching schema owns
134
+ both serialization and `httpApiStatus` (500 when unannotated). If its encoder fails,
135
+ the request fails as a server error; another schema is not tried. `headers(error)`
136
+ can add a Bearer challenge.
137
+ Responses use `Cache-Control: no-store`, including failures handled by enclosing middleware. Provide the returned middleware's `.layer` to
138
+ HTTP and MCP route layers. See [examples/app.ts](examples/app.ts).
139
+
140
+ Authentication dependencies are request requirements, as with native `HttpRouter.middleware`.
141
+ Use `.combine(...)` with middleware that provides them. Acquired resources stay alive
142
+ until the request scope closes. Downstream action errors are handled by their transport,
143
+ not serialized as authentication failures.
144
+
145
+ `ActionMcp.protectedResource({ resource, authorizationServers, scopesSupported })`
146
+ returns `{ layer, metadataUrl, challenge }`. Mount its public `layer` separately from
147
+ protected routes. It publishes [RFC 9728 metadata](https://www.rfc-editor.org/rfc/rfc9728.html)
148
+ at `/.well-known/oauth-protected-resource` followed by the resource path and query. Discovery
149
+ matches that literal path and query for GET and HEAD; other requests reach the host router.
150
+ Caching policy belongs to the host. `challenge()`
151
+ returns the `WWW-Authenticate` value; pass `{ error: "invalid_token" }` or
152
+ `{ error: "insufficient_scope", scope: "admin" }` where appropriate. Supported scopes
153
+ are advertised in metadata; the challenge names only scopes needed for that request.
154
+ The helper supports HTTPS URLs and loopback HTTP for development, with no credentials
155
+ or fragment. Resource identifiers may include a query; authorization-server issuers may not.
156
+ Your application owns login, consent, and token verification.
157
+
158
+ ## MCP testing
159
+
160
+ Import `mcpRequest` from `@gjermundgaraba/effect-actions/testing`; it requires no MCP client dependency.
161
+ Import `withMcpClient` from `@gjermundgaraba/effect-actions/testing/client` and install
162
+ its optional peer `@modelcontextprotocol/client@^2.0.0`. Only the client entry point loads this peer.
163
+
164
+ - `mcpRequest(method, params?, { url, headers? })` builds a stateless 2026-07-28 request.
165
+ `url` is required. Caller `params._meta` fields override the default client capabilities
166
+ and information; application metadata is preserved. The protocol version stays pinned to
167
+ 2026-07-28 in both the request header and metadata. Metadata is merged shallowly.
168
+ - `withMcpClient(fetch, async client => ..., { path, mode?, baseUrl?, headers? })`
169
+ connects the official client and closes it in a `finally` block. `path` is required.
170
+ `mode` defaults to `"modern"` (2026-07-28); use `"legacy"` to exercise session negotiation.
@@ -0,0 +1,54 @@
1
+ import { Effect, Schema } from "effect";
2
+ declare namespace Action_d_exports {
3
+ export { Action, Any, Codec, Handler, McpOptions, NoInput, Options, SchemaErrorPolicy, SchemaFailure, make };
4
+ }
5
+ export type Codec = Schema.Codec<unknown, unknown, never, never>;
6
+ /** Failed request decoding or successful-result encoding, independent of transport terminology. */
7
+ export interface SchemaFailure {
8
+ readonly phase: "input" | "output";
9
+ /** May contain sensitive values. Do not reflect it in public error messages. */
10
+ readonly cause: Schema.SchemaError;
11
+ }
12
+ /** Pure application-owned mapping, shared by the HTTP and MCP adapters. */
13
+ export interface SchemaErrorPolicy<Errors extends ReadonlyArray<Codec>> {
14
+ readonly errors: Errors;
15
+ readonly map: (failure: SchemaFailure) => NoInfer<Errors[number]["Type"]>;
16
+ }
17
+ export interface McpOptions {
18
+ readonly name?: string;
19
+ readonly readOnly?: boolean;
20
+ /** Defaults to `!readOnly`; the MCP spec only defines it for non-read-only tools. */
21
+ readonly destructive?: boolean;
22
+ }
23
+ export interface Options<Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Http extends boolean = boolean> {
24
+ readonly description: string;
25
+ /** Defaults to `NoInput`. */
26
+ readonly input?: Input;
27
+ readonly success: Output;
28
+ /** One schema per declared failure; each keeps its own HTTP status annotation. Defaults to none. */
29
+ readonly error?: Errors;
30
+ readonly http?: Http;
31
+ readonly mcp?: false | McpOptions;
32
+ }
33
+ /** A pure contract: schemas and transport metadata. Implementations are bound by `ActionGroup.implement`. */
34
+ export interface Action<Name extends string, Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Http extends boolean = boolean> {
35
+ readonly name: Name;
36
+ readonly description: string;
37
+ readonly input: Input;
38
+ readonly success: Output;
39
+ readonly errors: Errors;
40
+ readonly http: Http;
41
+ readonly mcp: false | {
42
+ readonly name: string;
43
+ readonly readOnly: boolean;
44
+ readonly destructive: boolean;
45
+ };
46
+ }
47
+ export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>>;
48
+ /** Receives decoded input; may fail only with the declared errors. */
49
+ export type Handler<A extends Any, R = never> = (input: A["input"]["Type"]) => Effect.Effect<A["success"]["Type"], A["errors"][number]["Type"], R>;
50
+ /** An empty object schema that also produces the object root MCP requires. */
51
+ export declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
52
+ export declare function make<const Name extends string, Input extends Codec = typeof NoInput, Output extends Codec = never, const Errors extends ReadonlyArray<Codec> = [], const Http extends boolean = true>(name: Name, options: Options<Input, Output, Errors, Http>): Action<Name, Input, Output, Errors, Http>;
53
+ //#endregion
54
+ export { Action_d_exports as t };
@@ -0,0 +1,30 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { Schema } from "effect";
3
+ //#region src/Action.ts
4
+ var Action_exports = /* @__PURE__ */ __exportAll({
5
+ NoInput: () => NoInput,
6
+ make: () => make
7
+ });
8
+ /** An empty object schema that also produces the object root MCP requires. */
9
+ const NoInput = Schema.Record(Schema.String, Schema.Never);
10
+ function make(name, options) {
11
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) throw new Error(`Invalid action name: ${name}`);
12
+ const readOnly = options.mcp === false ? false : options.mcp?.readOnly ?? false;
13
+ const mcp = options.mcp === false ? false : {
14
+ name: options.mcp?.name ?? name,
15
+ readOnly,
16
+ destructive: options.mcp?.destructive ?? !readOnly
17
+ };
18
+ if (mcp !== false && !/^[A-Za-z0-9_-]{1,128}$/.test(mcp.name)) throw new Error(`Invalid MCP name: ${mcp.name}`);
19
+ return {
20
+ name,
21
+ description: options.description,
22
+ input: options.input ?? NoInput,
23
+ success: options.success,
24
+ errors: options.error ?? [],
25
+ http: options.http !== false,
26
+ mcp
27
+ };
28
+ }
29
+ //#endregion
30
+ export { NoInput, make, Action_exports as t };
@@ -0,0 +1,21 @@
1
+ import { Any, Handler, t as Action_d_exports } from "./Action.mjs";
2
+ import { t as Implementation } from "./implementation-D75QAcD3.mjs";
3
+ import { Effect, Scope } from "effect";
4
+ declare namespace ActionGroup_d_exports {
5
+ export { Group, Implementation, make };
6
+ }
7
+ type HandlersFrom<Actions extends ReadonlyArray<Any>> = { readonly [A in Actions[number] as A["name"]]: Handler<A, any>; };
8
+ type HandlersContext<H> = { [K in keyof H]: H[K] extends ((input: never) => Effect.Effect<infer _A, infer _E, infer R>) ? R : never; }[keyof H];
9
+ export interface Group<Actions extends ReadonlyArray<Any>> {
10
+ readonly actions: Actions;
11
+ /**
12
+ * Bind every handler at once. Pass an Effect to resolve build-time services
13
+ * once (`const users = yield* Users`); services yielded inside a handler are
14
+ * request-scoped instead. Scoped acquisition lasts for the adapter runtime.
15
+ */
16
+ readonly implement: <H extends HandlersFrom<Actions>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Actions, HandlersContext<H>, EX, Exclude<RX, Scope.Scope>>;
17
+ }
18
+ /** Duplicate action and MCP names fail at definition time. */
19
+ export declare const make: <const Actions extends ReadonlyArray<Any>>(...actions: Actions) => Group<Actions>;
20
+ //#endregion
21
+ export { type Implementation, ActionGroup_d_exports as t };
@@ -0,0 +1,27 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { t as Implementation } from "./implementation-BkLqMWtv.mjs";
3
+ import { Effect } from "effect";
4
+ //#region src/ActionGroup.ts
5
+ var ActionGroup_exports = /* @__PURE__ */ __exportAll({ make: () => make });
6
+ /** Duplicate action and MCP names fail at definition time. */
7
+ const make = (...actions) => {
8
+ const names = /* @__PURE__ */ new Set();
9
+ const toolNames = /* @__PURE__ */ new Set();
10
+ for (const action of actions) {
11
+ if (names.has(action.name)) throw new Error(`Duplicate action: ${action.name}`);
12
+ names.add(action.name);
13
+ if (action.mcp !== false) {
14
+ if (toolNames.has(action.mcp.name)) throw new Error(`Duplicate MCP tool: ${action.mcp.name}`);
15
+ toolNames.add(action.mcp.name);
16
+ }
17
+ }
18
+ return {
19
+ actions,
20
+ implement: (build) => {
21
+ const built = Effect.isEffect(build) ? build : Effect.succeed(build);
22
+ return Implementation.make(actions, built);
23
+ }
24
+ };
25
+ };
26
+ //#endregion
27
+ export { make, ActionGroup_exports as t };
@@ -0,0 +1,27 @@
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 };
@@ -0,0 +1,31 @@
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 };
@@ -0,0 +1,53 @@
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 ADDED
@@ -0,0 +1,74 @@
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 };
@@ -0,0 +1,32 @@
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 };
@@ -0,0 +1,21 @@
1
+ import { Any, t as Action_d_exports } from "./Action.mjs";
2
+ import { Effect, Layer, Scope } from "effect";
3
+ //#region src/internal/implementation.d.ts
4
+ type Erased<R> = {
5
+ handle(input: unknown): Effect.Effect<unknown, unknown, R>;
6
+ }["handle"];
7
+ type Handlers<R> = Readonly<Record<string, Erased<R>>>;
8
+ /**
9
+ * Opaque implementation value; only its type is public. `R` is required per
10
+ * request, `EX`/`RX` describe handler acquisition at Layer build. Layer
11
+ * memoization lets both adapters share one handler build per runtime.
12
+ */
13
+ declare class Implementation<Actions extends ReadonlyArray<Any>, R, EX, RX> {
14
+ #private;
15
+ readonly actions: Actions;
16
+ private constructor();
17
+ static make<Actions extends ReadonlyArray<Any>, R, EX, RX>(actions: Actions, build: Effect.Effect<Handlers<R>, EX, RX>): Implementation<Actions, R, EX, Exclude<RX, Scope.Scope>>;
18
+ static register<Actions extends ReadonlyArray<Any>, R, EX, RX, Out, E, In>(app: Implementation<Actions, R, EX, RX>, make: (handlers: Handlers<R>) => Layer.Layer<Out, E, In>): Layer.Layer<Out, E | EX, In | RX>;
19
+ }
20
+ //#endregion
21
+ export { Implementation as t };
@@ -0,0 +1,6 @@
1
+ import { t as Action_d_exports } from "./Action.mjs";
2
+ import { t as ActionGroup_d_exports } from "./ActionGroup.mjs";
3
+ import { t as ActionHttp_d_exports } from "./http.mjs";
4
+ import { t as ActionMcp_d_exports } from "./mcp.mjs";
5
+ import { t as Authentication_d_exports } from "./authentication.mjs";
6
+ export { Action_d_exports as Action, ActionGroup_d_exports as ActionGroup, ActionHttp_d_exports as ActionHttp, ActionMcp_d_exports as ActionMcp, Authentication_d_exports as Authentication };
package/dist/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import { t as Action_exports } from "./Action.mjs";
2
+ import { t as ActionGroup_exports } from "./ActionGroup.mjs";
3
+ import { t as ActionHttp_exports } from "./http.mjs";
4
+ import { t as ActionMcp_exports } from "./mcp.mjs";
5
+ import { t as Authentication_exports } from "./authentication.mjs";
6
+ export { Action_exports as Action, ActionGroup_exports as ActionGroup, ActionHttp_exports as ActionHttp, ActionMcp_exports as ActionMcp, Authentication_exports as Authentication };
package/dist/mcp.d.mts ADDED
@@ -0,0 +1,46 @@
1
+ import { Any, Codec, SchemaErrorPolicy, t as Action_d_exports } from "./Action.mjs";
2
+ import { t as Implementation } from "./implementation-D75QAcD3.mjs";
3
+ import { Cause, Layer } from "effect";
4
+ import { McpProtocol } from "effect/unstable/ai";
5
+ import { HttpRouter } from "effect/unstable/http";
6
+ import { NonEmptyReadonlyArray } from "effect/Array";
7
+ declare namespace ActionMcp_d_exports {
8
+ export { BearerChallengeOptions, Options, ProtectedResourceOptions, layer, protectedResource, protocols };
9
+ }
10
+ export interface Options<Errors extends ReadonlyArray<Codec> = []> {
11
+ readonly schemaError?: SchemaErrorPolicy<Errors>;
12
+ readonly name: string;
13
+ readonly version: string;
14
+ readonly path: HttpRouter.PathInput;
15
+ readonly protocols?: NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>;
16
+ readonly allowedOrigins?: ReadonlyArray<string>;
17
+ readonly instructions?: string;
18
+ }
19
+ /** Protocol revisions served over Streamable HTTP; 2026-07-28 is stateless. */
20
+ export declare const protocols: NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>;
21
+ /** A Streamable HTTP MCP endpoint serving the group's MCP-enabled actions. */
22
+ export declare const layer: <Actions extends ReadonlyArray<Any>, R, EX, RX, Errors extends ReadonlyArray<Codec> = []>(app: Implementation<Actions, R, EX, RX>, options: Options<Errors>) => Layer.Layer<never, EX | Cause.IllegalArgumentError, RX | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", R>>;
23
+ export interface ProtectedResourceOptions {
24
+ /** Exact OAuth resource identifier. HTTPS, or HTTP on loopback for development. */
25
+ readonly resource: string;
26
+ readonly authorizationServers: NonEmptyReadonlyArray<string>;
27
+ readonly scopesSupported?: ReadonlyArray<string>;
28
+ readonly resourceName?: string;
29
+ }
30
+ export interface BearerChallengeOptions {
31
+ readonly error?: "invalid_token" | "insufficient_scope";
32
+ readonly errorDescription?: string;
33
+ /** Space-separated scopes needed for this request, not all supported scopes. */
34
+ readonly scope?: string;
35
+ }
36
+ /**
37
+ * Publish RFC 9728 discovery independently from the authenticated MCP route.
38
+ * This supplies metadata and challenges; the host still verifies access tokens.
39
+ */
40
+ export declare const protectedResource: (options: ProtectedResourceOptions) => {
41
+ layer: Layer.Layer<never, never, HttpRouter.HttpRouter>;
42
+ metadataUrl: string;
43
+ challenge: (challenge?: BearerChallengeOptions) => string;
44
+ };
45
+ //#endregion
46
+ export { ActionMcp_d_exports as t };
package/dist/mcp.mjs ADDED
@@ -0,0 +1,189 @@
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, JsonPointer, Layer, Schema } from "effect";
4
+ import { McpProtocol, McpSchema, McpServer, Tool } from "effect/unstable/ai";
5
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
6
+ //#region src/ActionMcp.ts
7
+ var ActionMcp_exports = /* @__PURE__ */ __exportAll({
8
+ layer: () => layer,
9
+ protectedResource: () => protectedResource,
10
+ protocols: () => protocols
11
+ });
12
+ /** Protocol revisions served over Streamable HTTP; 2026-07-28 is stateless. */
13
+ const protocols = [
14
+ McpProtocol.v2026_07_28,
15
+ McpProtocol.v2025_11_25,
16
+ McpProtocol.v2025_06_18,
17
+ McpProtocol.v2025_03_26,
18
+ McpProtocol.v2024_11_05
19
+ ];
20
+ const toolResult = (structuredContent, isError) => new McpSchema.CallToolResult({
21
+ isError,
22
+ structuredContent,
23
+ content: [{
24
+ type: "text",
25
+ text: JSON.stringify(structuredContent)
26
+ }]
27
+ });
28
+ const resolveReference = (name, what, schema, definitions) => {
29
+ if (typeof schema.$ref !== "string") return schema;
30
+ const reference = schema.$ref;
31
+ const path = JsonPointer.parseUriFragment(reference);
32
+ const key = path?.[1];
33
+ if (path?.length !== 2 || path[0] !== "$defs" || key === void 0) throw new Error(`${name}: unsupported MCP ${what} reference ${reference}`);
34
+ const definition = Object.hasOwn(definitions, key) ? definitions[key] : void 0;
35
+ if (definition === void 0) throw new Error(`${name}: missing MCP ${what} reference ${reference}`);
36
+ return definition;
37
+ };
38
+ /** MCP `inputSchema` requires a literal object root. */
39
+ const inputJsonSchema = (action) => {
40
+ const { schema, definitions } = Schema.toJsonSchemaDocument(action.input);
41
+ const root = resolveReference(action.name, "input", schema, definitions);
42
+ if (root.type !== "object") throw new Error(`${action.name}: MCP input must have an object root; use Action.NoInput for no arguments`);
43
+ return Object.keys(definitions).length === 0 ? root : {
44
+ ...root,
45
+ $defs: definitions
46
+ };
47
+ };
48
+ /** Declared errors are published as-is in `structuredContent`, so each must encode to an object. */
49
+ const assertObjectError = (owner, error) => {
50
+ const { schema, definitions } = Schema.toJsonSchemaDocument(error);
51
+ const visited = /* @__PURE__ */ new Set();
52
+ const encodesObject = (candidate) => {
53
+ const root = resolveReference(owner, "error", candidate, definitions);
54
+ if (root.type === "object" || visited.has(root)) return true;
55
+ visited.add(root);
56
+ const members = root.anyOf ?? root.oneOf;
57
+ return Array.isArray(members) && members.length > 0 && members.every(encodesObject);
58
+ };
59
+ if (!encodesObject(schema)) throw new Error(`${owner}: MCP error must have an object root`);
60
+ };
61
+ const registerTool = (action, table, policy) => Effect.gen(function* () {
62
+ if (action.mcp === false) return;
63
+ const server = yield* McpServer.McpServer;
64
+ const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(inputJsonSchema(action)).pipe(Effect.orDie);
65
+ const errors = [...action.errors, ...policy?.errors ?? []];
66
+ for (const error of action.errors) assertObjectError(action.name, error);
67
+ const input = Schema.toCodecJson(action.input);
68
+ const success = Schema.toCodecJson(action.success);
69
+ const failure = Schema.toCodecJson(Schema.Union(errors));
70
+ const handle = handlerFor(table, action);
71
+ const failureResult = (error) => Schema.encodeUnknownEffect(failure)(error).pipe(Effect.orDie, Effect.map((json) => toolResult(json, true)));
72
+ const schemaFailure = (phase, cause) => {
73
+ if (policy !== void 0) return failureResult(policy.map({
74
+ phase,
75
+ cause
76
+ }));
77
+ if (phase === "input") return Effect.fail(new McpSchema.InvalidParams({ message: cause.message }));
78
+ return Effect.die(cause);
79
+ };
80
+ const successResult = (value) => Schema.encodeUnknownEffect(success)(value).pipe(Effect.matchEffect({
81
+ onFailure: (cause) => schemaFailure("output", cause),
82
+ onSuccess: (json) => Effect.succeed(toolResult({ value: json }, false))
83
+ }));
84
+ yield* server.addTool({
85
+ tool: new McpSchema.Tool({
86
+ name: action.mcp.name,
87
+ description: action.description,
88
+ inputSchema,
89
+ outputSchema: Tool.getJsonSchemaFromSchema(Schema.Struct({ value: action.success })),
90
+ annotations: {
91
+ readOnlyHint: action.mcp.readOnly,
92
+ destructiveHint: action.mcp.destructive
93
+ }
94
+ }),
95
+ annotations: Context.empty(),
96
+ handle: (payload) => Schema.decodeUnknownEffect(input)(payload).pipe(Effect.matchEffect({
97
+ onFailure: (cause) => schemaFailure("input", cause),
98
+ onSuccess: (decoded) => handle(decoded).pipe(Effect.matchEffect({
99
+ onSuccess: successResult,
100
+ onFailure: failureResult
101
+ }))
102
+ }))
103
+ });
104
+ });
105
+ const registrationRouter = (router, context) => HttpRouter.HttpRouter.of({
106
+ ...router,
107
+ add: (method, path, handler, options) => router.add(method, path, handler, options).pipe(Effect.provideContext(context)),
108
+ addAll: (routes) => router.addAll(routes).pipe(Effect.provideContext(context)),
109
+ prefixed: (prefix) => registrationRouter(router.prefixed(prefix), context)
110
+ });
111
+ /** A Streamable HTTP MCP endpoint serving the group's MCP-enabled actions. */
112
+ const layer = (app, options) => Implementation.register(app, (table) => {
113
+ const native = Layer.effectDiscard(Effect.gen(function* () {
114
+ if (app.actions.some((action) => action.mcp !== false)) for (const error of options.schemaError?.errors ?? []) assertObjectError("Schema-error policy", error);
115
+ yield* Effect.forEach(app.actions, (action) => registerTool(action, table, options.schemaError), { discard: true });
116
+ })).pipe(Layer.provide(McpServer.layerHttp({
117
+ name: options.name,
118
+ version: options.version,
119
+ instructions: options.instructions,
120
+ path: options.path,
121
+ protocols: options.protocols ?? protocols,
122
+ allowedOrigins: options.allowedOrigins
123
+ })), Layer.fresh);
124
+ return Layer.fromBuildMemo((memoMap, scope) => Effect.gen(function* () {
125
+ const router = registrationRouter(yield* HttpRouter.HttpRouter, yield* Effect.context());
126
+ return yield* Layer.buildWithMemoMap(native, memoMap, scope).pipe(Effect.setContext(Context.make(HttpRouter.HttpRouter, router)));
127
+ }));
128
+ });
129
+ const oauthUrl = (value) => {
130
+ const url = new URL(value);
131
+ const loopback = [
132
+ "localhost",
133
+ "127.0.0.1",
134
+ "[::1]"
135
+ ].includes(url.hostname);
136
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username !== "" || url.password !== "" || value.includes("#")) throw new Error("OAuth URLs must use HTTPS (or loopback HTTP) without credentials or fragment");
137
+ return url;
138
+ };
139
+ const scopeToken = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
140
+ /**
141
+ * Publish RFC 9728 discovery independently from the authenticated MCP route.
142
+ * This supplies metadata and challenges; the host still verifies access tokens.
143
+ */
144
+ const protectedResource = (options) => {
145
+ const resource = oauthUrl(options.resource);
146
+ if (options.authorizationServers.length === 0) throw new Error("An authorization server is required");
147
+ for (const issuer of options.authorizationServers) {
148
+ oauthUrl(issuer);
149
+ if (issuer.includes("?")) throw new Error("Authorization server issuers must not contain a query");
150
+ }
151
+ for (const scope of options.scopesSupported ?? []) if (!scopeToken.test(scope)) throw new Error("Invalid OAuth scope token");
152
+ const path = `/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`;
153
+ const discoveryUrl = new URL(resource);
154
+ discoveryUrl.pathname = path;
155
+ const metadataUrl = discoveryUrl.href;
156
+ const target = metadataUrl.slice(discoveryUrl.origin.length);
157
+ const metadata = {
158
+ resource: options.resource,
159
+ authorization_servers: options.authorizationServers,
160
+ bearer_methods_supported: ["header"],
161
+ ...options.scopesSupported?.length ? { scopes_supported: options.scopesSupported } : {},
162
+ ...options.resourceName === void 0 ? {} : { resource_name: options.resourceName }
163
+ };
164
+ const response = HttpServerResponse.jsonUnsafe(metadata);
165
+ return {
166
+ layer: HttpRouter.middleware((next) => Effect.gen(function* () {
167
+ const request = yield* HttpServerRequest.HttpServerRequest;
168
+ const url = new URL(request.url, resource.origin);
169
+ if ((request.method === "GET" || request.method === "HEAD") && url.href.slice(url.origin.length) === target) return response;
170
+ return yield* next;
171
+ }), { global: true }),
172
+ metadataUrl,
173
+ challenge: (challenge = {}) => {
174
+ const parameters = [`resource_metadata="${metadataUrl.replace(/["\\]/g, "\\$&")}"`];
175
+ if (challenge.error !== void 0) parameters.push(`error="${challenge.error}"`);
176
+ if (challenge.errorDescription !== void 0) {
177
+ if (!/^[\x20-\x21\x23-\x5B\x5D-\x7E]*$/.test(challenge.errorDescription)) throw new Error("Invalid OAuth error description");
178
+ parameters.push(`error_description="${challenge.errorDescription}"`);
179
+ }
180
+ if (challenge.scope !== void 0) {
181
+ if (!challenge.scope.split(" ").every((scope) => scopeToken.test(scope))) throw new Error("Invalid OAuth challenge scope");
182
+ parameters.push(`scope="${challenge.scope}"`);
183
+ }
184
+ return `Bearer ${parameters.join(", ")}`;
185
+ }
186
+ };
187
+ };
188
+ //#endregion
189
+ export { layer, protectedResource, protocols, ActionMcp_exports as t };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,11 @@
1
+ import { Client } from "@modelcontextprotocol/client";
2
+ //#region src/TestingClient.d.ts
3
+ export interface McpClientOptions {
4
+ readonly path: string;
5
+ readonly mode?: "legacy" | "modern";
6
+ readonly baseUrl?: string | URL;
7
+ readonly headers?: ConstructorParameters<typeof Headers>[0];
8
+ }
9
+ /** Connect an official client and always close its transport after the callback. */
10
+ export declare const withMcpClient: <A>(fetch: (request: Request) => Promise<Response>, run: (client: Client) => Promise<A>, { mode, path, baseUrl, headers }: McpClientOptions) => Promise<A>;
11
+ //#endregion
@@ -0,0 +1,20 @@
1
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
2
+ //#region src/TestingClient.ts
3
+ /** Connect an official client and always close its transport after the callback. */
4
+ const withMcpClient = async (fetch, run, { mode = "modern", path, baseUrl = "http://localhost", headers }) => {
5
+ const client = new Client({
6
+ name: "test",
7
+ version: "0"
8
+ }, { versionNegotiation: { mode: mode === "modern" ? { pin: "2026-07-28" } : "legacy" } });
9
+ try {
10
+ await client.connect(new StreamableHTTPClientTransport(new URL(path, baseUrl), {
11
+ ...headers === void 0 ? {} : { requestInit: { headers } },
12
+ fetch: (input, init) => fetch(new Request(input, init))
13
+ }));
14
+ return await run(client);
15
+ } finally {
16
+ await client.close();
17
+ }
18
+ };
19
+ //#endregion
20
+ export { withMcpClient };
@@ -0,0 +1,10 @@
1
+ //#region src/Testing.d.ts
2
+ /** A stateless 2026-07-28 request. */
3
+ export interface McpRequestOptions {
4
+ readonly url: string | URL;
5
+ readonly headers?: ConstructorParameters<typeof Headers>[0];
6
+ }
7
+ export declare const mcpRequest: (method: string, params: (Record<string, unknown> & {
8
+ readonly _meta?: Record<string, unknown>;
9
+ }) | undefined, options: McpRequestOptions) => Request;
10
+ //#endregion
@@ -0,0 +1,34 @@
1
+ //#region src/Testing.ts
2
+ const mcpRequest = (method, params = {}, options) => {
3
+ const headers = new Headers(options.headers);
4
+ for (const [name, value] of Object.entries({
5
+ "content-type": "application/json",
6
+ accept: "application/json, text/event-stream",
7
+ "mcp-protocol-version": "2026-07-28",
8
+ "mcp-method": method,
9
+ ...typeof params.name === "string" ? { "mcp-name": params.name } : {}
10
+ })) headers.set(name, value);
11
+ return new Request(options.url, {
12
+ method: "POST",
13
+ headers,
14
+ body: JSON.stringify({
15
+ jsonrpc: "2.0",
16
+ id: 1,
17
+ method,
18
+ params: {
19
+ ...params,
20
+ _meta: {
21
+ "io.modelcontextprotocol/clientCapabilities": {},
22
+ "io.modelcontextprotocol/clientInfo": {
23
+ name: "test",
24
+ version: "0"
25
+ },
26
+ ...params._meta,
27
+ "io.modelcontextprotocol/protocolVersion": "2026-07-28"
28
+ }
29
+ }
30
+ })
31
+ });
32
+ };
33
+ //#endregion
34
+ export { mcpRequest };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@gjermundgaraba/effect-actions",
3
+ "version": "0.1.0-rc.0",
4
+ "description": "Define Effect action contracts once and expose them over HTTP and MCP.",
5
+ "homepage": "https://github.com/gjermundgaraba/effect-actions#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/gjermundgaraba/effect-actions/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/gjermundgaraba/effect-actions.git"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": "./dist/index.mjs",
20
+ "./Action": "./dist/Action.mjs",
21
+ "./ActionGroup": "./dist/ActionGroup.mjs",
22
+ "./authentication": "./dist/authentication.mjs",
23
+ "./http": "./dist/http.mjs",
24
+ "./mcp": "./dist/mcp.mjs",
25
+ "./testing": "./dist/testing.mjs",
26
+ "./testing/client": "./dist/testing/client.mjs",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "devDependencies": {
33
+ "@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect/@effect/platform-node@4e4a3a6",
34
+ "@modelcontextprotocol/client": "2.0.0",
35
+ "@types/node": "26.5.1",
36
+ "effect": "https://pkg.pr.new/Effect-TS/effect/effect@4e4a3a6",
37
+ "tsx": "4.23.13",
38
+ "typescript": "7.0.2",
39
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.2",
40
+ "vite-plus": "0.3.2"
41
+ },
42
+ "peerDependencies": {
43
+ "@modelcontextprotocol/client": "^2.0.0",
44
+ "effect": "4.0.0-rc.115"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@modelcontextprotocol/client": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devEngines": {
52
+ "packageManager": {
53
+ "name": "pnpm",
54
+ "version": "12.4.1",
55
+ "onFail": "download"
56
+ }
57
+ },
58
+ "engines": {
59
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
60
+ },
61
+ "scripts": {
62
+ "build": "vp pack",
63
+ "dev": "vp pack --watch",
64
+ "test": "vp test",
65
+ "check": "vp check",
66
+ "example": "node --import tsx examples/server.ts",
67
+ "test:package": "vp run build && node scripts/test-package.mjs"
68
+ }
69
+ }