@gjermundgaraba/effect-actions 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/ActionCatalog.mjs +2 -2
- package/dist/ActionCli.mjs +5 -1
- package/dist/ActionGroup.d.mts +14 -10
- package/dist/ActionGroup.mjs +12 -1
- package/dist/ActionHttp.d.mts +6 -0
- package/dist/ActionHttp.mjs +13 -7
- package/dist/ActionHttpClient.d.mts +45 -0
- package/dist/ActionHttpClient.mjs +24 -0
- package/dist/ActionMcp.d.mts +11 -22
- package/dist/ActionMcp.mjs +12 -8
- package/dist/Testing.d.mts +31 -1
- package/dist/Testing.mjs +50 -2
- package/dist/TestingClient.d.mts +6 -5
- package/dist/TestingClient.mjs +6 -3
- package/dist/internal/actions.d.mts +18 -7
- package/dist/internal/actions.mjs +11 -1
- package/dist/internal/implementation.mjs +3 -2
- package/dist/internal/tools.mjs +1 -1
- package/docs/ActionGroup.md +24 -16
- package/docs/ActionHttp.md +7 -5
- package/docs/ActionHttpClient.md +73 -0
- package/docs/ActionMcp.md +18 -21
- package/docs/CONTEXT.md +3 -2
- package/docs/README.md +18 -18
- package/docs/Testing.md +27 -16
- package/docs/guarantees.md +4 -4
- package/docs/setup.md +7 -6
- package/package.json +5 -4
package/docs/ActionGroup.md
CHANGED
|
@@ -15,18 +15,20 @@ Import `@gjermundgaraba/effect-actions/ActionGroup`.
|
|
|
15
15
|
| `app.group`, `app.build` | Bound contract and scoped handler acquisition. |
|
|
16
16
|
| `contracts(...groups)` | Exact action types keyed by `<group>.<action>`. |
|
|
17
17
|
|
|
18
|
-
Exported types: `Group`, `Any`, `Options`, `Implementation`, `Contracts`, `SchemaErrorPolicy
|
|
18
|
+
Exported types: `Group`, `Any`, `Options`, `Implementation`, `Contracts`, `SchemaErrorPolicy`,
|
|
19
|
+
`SchemaErrorAnswer`.
|
|
19
20
|
|
|
20
|
-
The optional `schemaError` group option
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
The optional `schemaError` group option is a `SchemaErrorPolicy`: two answers, `invalid` and
|
|
22
|
+
`internal`, each a `schema` (a declared error codec) and `make(failure)` (the native
|
|
23
|
+
`HttpApiSchemaError` to that error's decoded value). The library decides which answer applies.
|
|
24
|
+
It affects HTTP only. Handler records require every action key. Each handler's input, success
|
|
25
|
+
and declared errors come from its action; request requirements stay distinct from the
|
|
26
|
+
builder's requirements.
|
|
24
27
|
|
|
25
28
|
## Canonical
|
|
26
29
|
|
|
27
30
|
```ts
|
|
28
31
|
import { Effect, Schema } from "effect";
|
|
29
|
-
import type { HttpApiError } from "effect/unstable/httpapi";
|
|
30
32
|
import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
|
|
31
33
|
import { CurrentActor } from "./auth.js";
|
|
32
34
|
import { GetUser, RenameUser, WhoAmI } from "./contracts.js";
|
|
@@ -43,13 +45,17 @@ class InternalError extends Schema.TaggedError<InternalError>()(
|
|
|
43
45
|
{ httpApiStatus: 500 },
|
|
44
46
|
) {}
|
|
45
47
|
|
|
46
|
-
//
|
|
48
|
+
// A request that does not decode is the caller's fault; a result that does not
|
|
49
|
+
// encode is the server's. Shared across groups as a constant.
|
|
47
50
|
const schemaError = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
invalid: {
|
|
52
|
+
schema: InvalidRequest,
|
|
53
|
+
make: () => new InvalidRequest({ message: "The request does not match the action's input." }),
|
|
54
|
+
},
|
|
55
|
+
internal: {
|
|
56
|
+
schema: InternalError,
|
|
57
|
+
make: () => new InternalError({ message: "The request could not be completed." }),
|
|
58
|
+
},
|
|
53
59
|
};
|
|
54
60
|
|
|
55
61
|
export const UserActions = ActionGroup.make(
|
|
@@ -87,7 +93,7 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
87
93
|
|
|
88
94
|
- `name` matches `[A-Za-z0-9_-]+`, cannot be `then`, and must be unique within one `ActionHttp.make`. Action names must be unique within the group. MCP tool names must be unique within the group and within each Toolkit or MCP projection that serves it. Duplicates fail at `make`.
|
|
89
95
|
- Group `errors` are appended to each action's own `errors`, so every handler of the group may fail with them. A failure that only the surface produces belongs on the adapter instead (`ActionHttp.make`'s `errors`, `ActionMcp`'s and `ActionToolkit`'s `errors`), not here.
|
|
90
|
-
- `implement` takes a complete record or an Effect producing one. Every action must have a handler; missing keys are compile errors. Additional keys are not dispatched. It takes nothing else.
|
|
96
|
+
- `implement` takes a complete record or an Effect producing one. Every action must have a handler; missing keys are compile errors. A record that still lacks one at runtime (plain JavaScript, a cast) throws at `implement`; a builder's record fails the adapter layer's build. Nothing is served with a handler missing. Additional keys are not dispatched. It takes nothing else.
|
|
91
97
|
- The builder Effect runs once per adapter layer that serves the implementation, in that layer's scope. An implementation served by HTTP and MCP is built twice. Acquire shared state in a Layer you provide to the adapters, not in the builder.
|
|
92
98
|
- Services yielded in the builder are build-time requirements (`RX`). Services yielded in a handler are request-time requirements (`R` of the handler). Adapters keep these separate in their types. Use distinct tags for each kind; never provide a request-identity tag at startup (see [guarantees.md](guarantees.md)).
|
|
93
99
|
- `Implementation` is nominal. Spreading its properties does not produce an implementation. Each `implement` call is a separate binding.
|
|
@@ -97,8 +103,9 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
97
103
|
### Schema-error policy
|
|
98
104
|
|
|
99
105
|
- Without a policy, HTTP answers decoding and encoding failures with Effect's native empty 400.
|
|
100
|
-
- With a policy,
|
|
101
|
-
-
|
|
106
|
+
- With a policy, the library picks the answer from the native `HttpApiSchemaError`'s `kind`. Request-side kinds (`Payload`, `Params`, `Headers`, `Query`: the request did not decode) get `invalid`; response-side kinds (`Body`, `ResponseHeaders`: the handler's result did not encode) get `internal`. `make` receives the failure (`kind`, `cause`) and returns its schema's value. HTTP uses that error's `httpApiStatus`, and both errors appear in `Http.api`, in clients, and in OpenAPI.
|
|
107
|
+
- In practice only `Payload` and `Body` occur: actions declare no parameters, query, or headers, and no response headers.
|
|
108
|
+
- `invalid` and `internal` may share one schema; it is declared once.
|
|
102
109
|
- MCP is unaffected. The native `McpServer` answers invalid arguments and unencodable results itself.
|
|
103
110
|
- Policy errors extend the transport contract, not the handler contract. A handler cannot return them.
|
|
104
111
|
- The policy runs only on the server. Client-side codec failures stay `SchemaError`.
|
|
@@ -113,6 +120,7 @@ const renames: "renameUser" = contracts["users.renameUser"].name;
|
|
|
113
120
|
- `Property 'x' is missing in type` at `implement`: the record lacks a handler for action `x`.
|
|
114
121
|
- Handler compiles but returns an error not in `errors`: type error on the handler's error channel. Declare it on the action or the group.
|
|
115
122
|
- `Expected 1 arguments, but got 2` at `implement`: the second options object is gone. Move `before` to the adapter that serves the group.
|
|
116
|
-
- `
|
|
123
|
+
- `Parameter 'failure' implicitly has an 'any' type` in a standalone policy constant: a `make` that reads the failure is typed only inline. Annotate the parameter as `HttpApiError.HttpApiSchemaError` (type import from `effect/unstable/httpapi`), or annotate the constant as `ActionGroup.SchemaErrorPolicy<typeof Invalid, typeof Internal>`.
|
|
124
|
+
- `Missing handlers for group "<group>": <actions>` thrown at `implement`, or failing the layer build: the handler record lacks those actions. Add them.
|
|
117
125
|
- A service is resolved once and shared across requests when it should be per request: it was yielded in the builder. Move the `yield*` into the handler.
|
|
118
126
|
- `Duplicate contract group` thrown by `contracts`: two groups share a name, so their keys would collide. Rename one.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -13,6 +13,7 @@ Import `@gjermundgaraba/effect-actions/ActionHttp`.
|
|
|
13
13
|
| `Http.groups` | The exact bound contracts, in declaration order. |
|
|
14
14
|
| `Http.api` | Native Effect `HttpApi` for clients and OpenAPI. |
|
|
15
15
|
| `Http.layer(apps, options?)` | Serve a readonly collection of implementations; omit options when no hook is needed. |
|
|
16
|
+
| `Http.openApi(path?)` | Serve the OpenAPI document with `GET path`; defaults to `<apiPath>/openapi.json`. |
|
|
16
17
|
|
|
17
18
|
Exported types: `Options`, `LayerOptions`, `Http`, `Api`.
|
|
18
19
|
|
|
@@ -32,8 +33,7 @@ Route shape: `POST <apiPath>/<group>/<action>`. Operation ID: `<group>.<action>`
|
|
|
32
33
|
|
|
33
34
|
```ts
|
|
34
35
|
import { Layer } from "effect";
|
|
35
|
-
import {
|
|
36
|
-
import { HttpApiScalar, HttpApiSwagger, OpenApi } from "effect/unstable/httpapi";
|
|
36
|
+
import { HttpApiScalar, HttpApiSwagger } from "effect/unstable/httpapi";
|
|
37
37
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
38
38
|
import { PublicActions, UserActions } from "./contracts.js";
|
|
39
39
|
import { PublicApp, UserApp } from "./handlers.js";
|
|
@@ -56,9 +56,9 @@ const routes = Layer.mergeAll(
|
|
|
56
56
|
Http.layer([UserApp], { before: authorize }).pipe(Layer.provide(authentication.layer)),
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
// Documents are Effect's own, reading the same contract.
|
|
59
|
+
// Documents are Effect's own, reading the same contract: `GET /api/actions/openapi.json`.
|
|
60
60
|
const documentation = Layer.mergeAll(
|
|
61
|
-
|
|
61
|
+
Http.openApi(),
|
|
62
62
|
HttpApiSwagger.layer(Http.api, { path: "/docs" }),
|
|
63
63
|
HttpApiScalar.layer(Http.api, { path: "/reference" }),
|
|
64
64
|
);
|
|
@@ -70,7 +70,8 @@ Serve with `HttpRouter.serve(layer).pipe(Layer.provide(NodeHttpServer.layer(crea
|
|
|
70
70
|
|
|
71
71
|
### Client
|
|
72
72
|
|
|
73
|
-
The client is Effect's native grouped `HttpApiClient` over `Http.api
|
|
73
|
+
The client is Effect's native grouped `HttpApiClient` over `Http.api`. Code that does not
|
|
74
|
+
run Effects uses the Promise client in [ActionHttpClient.md](ActionHttpClient.md).
|
|
74
75
|
|
|
75
76
|
```ts
|
|
76
77
|
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
@@ -98,6 +99,7 @@ export const greeting = Effect.gen(function* () {
|
|
|
98
99
|
- A hook refusal and a handler error are plain declared errors: a JSON body and a status, no other headers. Challenge headers such as `WWW-Authenticate` belong to the admission middleware that runs before the router reaches these routes (see [Authentication.md](Authentication.md)), which sets them on its own response.
|
|
99
100
|
- Request-time handler services are `HttpRouter.Request.From<"Requires", R>`. Supply them with router middleware (`Authentication.middleware`, `HttpRouter.middleware`), `HttpRouter.provideRequest`, or the request context. Build-time services are ordinary layer requirements.
|
|
100
101
|
- `Http.api` is a plain `HttpApi`. Anything Effect can do with an `HttpApi` works: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `HttpApi.addHttpApi` to combine with other APIs, `HttpApiClient.make`.
|
|
102
|
+
- `Http.openApi(path?)` is `OpenApi.fromApi(Http.api)` as one `GET` route, `<apiPath>/openapi.json` unless a path is given. It documents every bound group, not only the served ones. It is a plain route: middleware provided to its layer covers it, and nothing covers it otherwise.
|
|
101
103
|
- Client calls always take `{ payload }`. No-input actions take `{ payload: {} }`. Pass `null` or `undefined` only when the codec accepts it. There is no flat client.
|
|
102
104
|
- Client effects fail with the declared errors, the group's policy errors, the binding's `errors`, `SchemaError` for local codec failures, and native `HttpClientError`. MCP-only actions are absent from the client.
|
|
103
105
|
- Add authentication headers with the native `transformClient` option. Use `HttpApiClient.makeWith` for custom error or service channels. Native per-call response modes are available.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ActionHttpClient
|
|
2
|
+
|
|
3
|
+
A Promise client for an HTTP binding, for code that does not run Effects, such as a browser
|
|
4
|
+
page. It is Effect's native `HttpApiClient` over `fetch`, built once; each call is one request.
|
|
5
|
+
|
|
6
|
+
## API
|
|
7
|
+
|
|
8
|
+
Import `@gjermundgaraba/effect-actions/ActionHttpClient`.
|
|
9
|
+
|
|
10
|
+
| API | Purpose |
|
|
11
|
+
| ------------------------- | ----------------------------------------------------------------------------- |
|
|
12
|
+
| `promise(http, options?)` | `client.<group>.<action>(input)` for every HTTP-served action of the binding. |
|
|
13
|
+
| `Options` | Where and how requests are sent. |
|
|
14
|
+
| `Client`, `Method` | The client's type, derived from the binding, and one action's method. |
|
|
15
|
+
|
|
16
|
+
| Option | Meaning |
|
|
17
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `baseUrl` | What routes are resolved against, such as `https://api.example.com`. Omitted: relative routes (the page's origin in a browser). |
|
|
19
|
+
| `token` | Sent as `Authorization: Bearer <token>` with every call. |
|
|
20
|
+
| `fetch` | The transport. Defaults to the global `fetch`, looked up on each call. |
|
|
21
|
+
|
|
22
|
+
A method takes the action's decoded input and resolves with its decoded success. An action
|
|
23
|
+
whose input may be empty (no `input`, or only optional fields) may be called without an argument.
|
|
24
|
+
|
|
25
|
+
## Canonical
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { HttpClientError } from "effect/unstable/http";
|
|
29
|
+
import * as ActionHttpClient from "@gjermundgaraba/effect-actions/ActionHttpClient";
|
|
30
|
+
import { Http, UserNotFound } from "./contracts.js";
|
|
31
|
+
|
|
32
|
+
// Promises in, Promises out: for code that does not run Effects, such as a browser page.
|
|
33
|
+
const client = ActionHttpClient.promise(Http, {
|
|
34
|
+
baseUrl: "http://127.0.0.1:3000",
|
|
35
|
+
token: "alice",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const userName = async (id: string): Promise<string> => {
|
|
39
|
+
try {
|
|
40
|
+
return (await client.users.getUser({ id })).name;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// A declared error rejects as its own value...
|
|
43
|
+
if (error instanceof UserNotFound) return "(no such user)";
|
|
44
|
+
|
|
45
|
+
// ...and what the contract cannot account for as Effect's own `HttpClientError`.
|
|
46
|
+
if (HttpClientError.isHttpClientError(error) && error.response === undefined) {
|
|
47
|
+
return "(server unreachable)";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Other headers, or reacting to a response every call may meet (a proxy's 401), go through
|
|
56
|
+
`fetch`: `fetch: (input, init) => { const request = new Request(input, init); request.headers.set("x-agent", agent); return fetch(request); }`.
|
|
57
|
+
|
|
58
|
+
## Rules
|
|
59
|
+
|
|
60
|
+
- A call rejects with exactly what the native client's Effect fails with. Declared errors arrive as their decoded values: the action's own (its group's `errors` included), its group's schema-error policy's, and the binding's surface `errors`. Classify them with `instanceof`, their `_tag`, or `Schema.is`.
|
|
61
|
+
- Anything the contract does not account for rejects with Effect's own error. `HttpClientError`: the server could not be reached (`response` is `undefined`), or answered with a status no schema declares, or with a body that could not be read. `SchemaError`: the input did not encode, or the success body did not decode.
|
|
62
|
+
- The library adds no error type of its own and interprets no status. Which failures mean "signed out" or "try again" is the caller's decision.
|
|
63
|
+
- Nothing is retried. A rejected write may or may not have happened; only a declared error says what the server did.
|
|
64
|
+
- Only HTTP-served actions have methods. An action with `http: false` is absent, and so is a group without HTTP actions.
|
|
65
|
+
- The client holds no connections or timers. Making one is synchronous and sends nothing.
|
|
66
|
+
- A method is not an Effect. Code that runs Effects uses `HttpApiClient.make(Http.api)` directly (see [ActionHttp.md](ActionHttp.md)); its methods take `{ payload }`.
|
|
67
|
+
|
|
68
|
+
## Failure modes
|
|
69
|
+
|
|
70
|
+
- Rejects with `HttpClientError` whose `reason._tag` is `StatusCodeError` for a 401 or 403: the surface answered with an error the binding does not declare. Add it to `ActionHttp.make`'s `errors`, and have the middleware encode it.
|
|
71
|
+
- Rejects with `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: `baseUrl` is omitted, and there is no page to resolve relative routes against. Set `baseUrl`.
|
|
72
|
+
- A stubbed global `fetch` is not used: `fetch` was passed as an option, which takes precedence.
|
|
73
|
+
- Property does not exist on the client: the action is `http: false`, or its group is not bound in this `Http`.
|
package/docs/ActionMcp.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP tools from implementations, on Effect's native `McpServer`. One `Tool` per MCP-enabled
|
|
4
4
|
action. Two transports: a Streamable HTTP endpoint mounted on the router, or newline-delimited
|
|
5
|
-
JSON-RPC on standard I/O for a subprocess.
|
|
5
|
+
JSON-RPC on standard I/O for a subprocess. Both speak MCP 2026-07-28 only.
|
|
6
6
|
|
|
7
7
|
## API
|
|
8
8
|
|
|
@@ -14,15 +14,19 @@ Import `@gjermundgaraba/effect-actions/ActionMcp`.
|
|
|
14
14
|
| `layerStdio(apps, options)` | Serve implementations over a subprocess's standard I/O. |
|
|
15
15
|
| `Options`, `StdioOptions` | Configuration for the two transports. |
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
17
|
+
The options are the native `McpServer.layerHttp` / `McpServer.layerStdio` options, except
|
|
18
|
+
`protocols`, plus this surface's `errors` and `before`. The native ones pass through unchanged.
|
|
19
|
+
|
|
20
|
+
| Option | Meaning |
|
|
21
|
+
| ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `name`, `version` | Required native server information. |
|
|
23
|
+
| `description`, `websiteUrl`, `icons` | Optional native server information, sent to clients with `name` and `version`. |
|
|
24
|
+
| `instructions` | Optional native server instructions. |
|
|
25
|
+
| `extensions` | Optional native server capability extensions. |
|
|
26
|
+
| `path` | Required HTTP endpoint path; HTTP only, no default. |
|
|
27
|
+
| `allowedOrigins` | Optional exact Origin allowlist; HTTP only, not CORS configuration. |
|
|
28
|
+
| `errors` | Optional surface error codecs, declared on every served tool. |
|
|
29
|
+
| `before` | Optional Effectful hook receiving the selected `Action.Any`; fails only with declared surface errors. |
|
|
26
30
|
|
|
27
31
|
Both layers retain served implementations' build failures and requirements, plus native
|
|
28
32
|
`IllegalArgumentError`. HTTP needs the router and wraps handler/hook services as request
|
|
@@ -33,7 +37,6 @@ is supplied by the server, not owed by the host.
|
|
|
33
37
|
|
|
34
38
|
```ts
|
|
35
39
|
import { Layer } from "effect";
|
|
36
|
-
import { McpProtocol } from "effect/unstable/ai";
|
|
37
40
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
38
41
|
import { AuditApp, PublicApp, UserApp } from "./handlers.js";
|
|
39
42
|
import { authentication, authorize, Forbidden } from "./auth.js";
|
|
@@ -43,7 +46,6 @@ const allowedOrigins = ["http://localhost:3000"];
|
|
|
43
46
|
// One endpoint is one route: its middleware covers every tool on it.
|
|
44
47
|
// Tools with different middleware needs get their own endpoint.
|
|
45
48
|
const publicMcp = ActionMcp.layerHttp([PublicApp], {
|
|
46
|
-
protocols: [McpProtocol.v2026_07_28],
|
|
47
49
|
name: "app-public",
|
|
48
50
|
version: "1.0.0",
|
|
49
51
|
path: "/mcp/public",
|
|
@@ -51,7 +53,6 @@ const publicMcp = ActionMcp.layerHttp([PublicApp], {
|
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
const mcp = ActionMcp.layerHttp([UserApp, AuditApp], {
|
|
54
|
-
protocols: [McpProtocol.v2026_07_28],
|
|
55
56
|
name: "app",
|
|
56
57
|
version: "1.0.0",
|
|
57
58
|
path: "/mcp",
|
|
@@ -73,7 +74,6 @@ choose its policy for every route it covers.
|
|
|
73
74
|
|
|
74
75
|
```ts
|
|
75
76
|
import { Effect, Layer } from "effect";
|
|
76
|
-
import { McpProtocol } from "effect/unstable/ai";
|
|
77
77
|
import { HttpRouter } from "effect/unstable/http";
|
|
78
78
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
79
79
|
import { Actions } from "./quickstart.js";
|
|
@@ -86,7 +86,6 @@ const mcp = ActionMcp.layerHttp([app], {
|
|
|
86
86
|
name: "greetings",
|
|
87
87
|
version: "1.0.0",
|
|
88
88
|
path: "/mcp",
|
|
89
|
-
protocols: [McpProtocol.v2026_07_28],
|
|
90
89
|
allowedOrigins,
|
|
91
90
|
});
|
|
92
91
|
|
|
@@ -114,7 +113,6 @@ export const routes = Layer.mergeAll(
|
|
|
114
113
|
```ts
|
|
115
114
|
import { NodeRuntime, NodeStdio } from "@effect/platform-node";
|
|
116
115
|
import { Console, Effect, Layer, Logger, Schema } from "effect";
|
|
117
|
-
import { McpProtocol } from "effect/unstable/ai";
|
|
118
116
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
119
117
|
import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
|
|
120
118
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
@@ -132,7 +130,6 @@ const app = ActionGroup.make({ name: "stdio" }, Status).implement({
|
|
|
132
130
|
const layer = ActionMcp.layerStdio([app], {
|
|
133
131
|
name: "effect-actions-stdio",
|
|
134
132
|
version: "0.1.0",
|
|
135
|
-
protocols: [McpProtocol.v2026_07_28],
|
|
136
133
|
}).pipe(Layer.provide(NodeStdio.layer));
|
|
137
134
|
|
|
138
135
|
// Protocol messages use stdout exclusively. Runtime diagnostics remain on stderr.
|
|
@@ -145,7 +142,7 @@ Layer.launch(layer).pipe(
|
|
|
145
142
|
|
|
146
143
|
## Rules
|
|
147
144
|
|
|
148
|
-
-
|
|
145
|
+
- Both transports serve MCP 2026-07-28 and no other revision; there is no `protocols` option. Over HTTP the endpoint is stateless: no initialize handshake, no session, and every request stands alone. Effect owns version checks and rejects any other revision.
|
|
149
146
|
- `path` has no default. `layerHttp` uses the single-endpoint Streamable HTTP transport, never the two-endpoint HTTP+SSE form, whatever revision is negotiated.
|
|
150
147
|
- Requests reaching the native MCP handler with an `Origin` header receive **403** unless that exact origin is listed in `allowedOrigins`. Requests without `Origin` pass this check. Authentication middleware wrapping the endpoint runs first and may reject the request before native Origin validation; the allowlist does not protect authentication from untrusted-origin requests.
|
|
151
148
|
- `allowedOrigins` is an Origin allowlist, not CORS configuration. Cross-origin browser clients also need outer CORS middleware or a proxy to handle preflight and add response headers. Without it, an allowed-origin `OPTIONS` request receives **405** and even a successful `POST` has no `Access-Control-Allow-Origin`. Keep preflight outside authentication and apply CORS headers to refusals too.
|
|
@@ -156,7 +153,7 @@ Layer.launch(layer).pipe(
|
|
|
156
153
|
- Invalid arguments and unencodable results are answered by the native `McpServer`: an `isError` result with a message for the model. Group `schemaError` policies do not apply.
|
|
157
154
|
- Defects and encoding failures produce the generic `isError` text `Tool execution failed due to an internal server error.`; the cause is logged, not sent.
|
|
158
155
|
- Tool discovery is not filtered by actor. Every caller sees every tool of the endpoint. Authorization happens in `before`.
|
|
159
|
-
- Cancellation is Effect's native RPC interruption.
|
|
156
|
+
- Cancellation is Effect's native RPC interruption. Over HTTP there is no session, so `notifications/cancelled` interrupts nothing; a tool call ends with its HTTP request, whose lifetime (for example, whether a client disconnect interrupts it) is the host's.
|
|
160
157
|
- Handlers may yield `McpSchema.McpRequestContext` for the client's declared information; the native server supplies it to every tool call, so it is never a router or host requirement.
|
|
161
158
|
- stdio: the host supplies `Stdio` (`NodeStdio.layer`) and any request-time services explicitly, including the trusted principal. There is no authentication middleware. Tool arguments never establish identity. Keep stdout for protocol messages only and route logs to stderr.
|
|
162
159
|
- Each endpoint or subprocess owns a fresh native tool registry. That isolates tool names, not application context.
|
|
@@ -170,9 +167,9 @@ Layer.launch(layer).pipe(
|
|
|
170
167
|
- Type error listing `HttpRouter.Request.From<"Requires", ...>`: a handler yields a request service and the endpoint has no middleware providing it. Provide it with `Layer.provide(middleware.layer)` on that `layerHttp`.
|
|
171
168
|
- Public tool requires a token: it shares an endpoint with protected tools. Give it its own path.
|
|
172
169
|
- Client reports a broken transport from a stdio subprocess: something printed to stdout. Set `Logger.LogToStderr` and remove `console.log`.
|
|
173
|
-
- Older MCP client cannot connect:
|
|
170
|
+
- Older MCP client cannot connect: over HTTP a request answers `400` with JSON-RPC error `-32020`, over stdio `initialize` answers JSON-RPC error `-32022`. The client speaks a 2025 revision, which opens with `initialize`. Only 2026-07-28 is served; pin the client to it (the official client: `versionNegotiation: { mode: { pin: "2026-07-28" } }`).
|
|
174
171
|
- An Origin-bearing request reaches the native handler and gets an empty 403: its `Origin` is not in `allowedOrigins`. Add the exact origin only if the deployment trusts it.
|
|
175
172
|
- A disallowed Origin receives 401 instead: wrapping authentication rejected it before the native Origin check. Put any required pre-authentication Host/Origin policy in outer host middleware.
|
|
176
173
|
- Browser calls fail despite an allowed Origin: configure CORS outside authentication and the MCP handler (see the browser example above). The native allowlist alone neither handles preflight nor adds CORS response headers.
|
|
177
|
-
-
|
|
174
|
+
- `Object literal may only specify known properties, and 'protocols'`: the revision is fixed. Delete the option.
|
|
178
175
|
- A refusal arrives as a generic internal-error result: the hook failed with an error this transport does not declare, which is a defect. Add its schema to `errors`.
|
package/docs/CONTEXT.md
CHANGED
|
@@ -19,13 +19,14 @@ use these words with these meanings.
|
|
|
19
19
|
- **Binding**: the value an adapter returns before anything runs. `ActionHttp.make` returns the HTTP binding (`groups`, `api`, `layer`); `ActionToolkit.make` returns the Toolkit binding (`toolkit`, `layer`).
|
|
20
20
|
- **Contract map**: the record `ActionGroup.contracts` returns, keyed `<group>.<action>`, with each action's exact type. The shared identity of an action across routes, tools and commands.
|
|
21
21
|
- **Declared error**: an error schema listed on an action or inherited from its group. Handlers may fail with it. HTTP encodes it with its `httpApiStatus`; MCP returns encoded error text in an `isError` result. Toolkit returns a native failure result, and CLI fails its command Effect without serializing the error.
|
|
22
|
-
- **Schema-error policy**: a group option
|
|
22
|
+
- **Schema-error policy**: a group option answering Effect's `HttpApiSchemaError` with a declared policy error and its status: `invalid` when the request did not decode, `internal` when the handler's result did not encode. The library decides which applies. HTTP only.
|
|
23
23
|
- **Policy error**: an error a schema-error policy may answer with. Part of the transport contract, never returnable by a handler.
|
|
24
24
|
- **Surface error**: an error declared on an adapter binding rather than on an action. Produced by middleware around the surface or by its pre-handler hook, never by a handler; declared so typed callers decode it.
|
|
25
25
|
- **Local-only action**: `http: false` and `mcp: false`. Reachable through `ActionCli` and listed in the catalog.
|
|
26
26
|
- **Tool**: the MCP or Toolkit projection of an MCP-enabled action, named by `mcp.name` and carrying its hints.
|
|
27
27
|
- **Endpoint**: one `ActionMcp.layerHttp` mount. One route, one middleware set, one tool registry.
|
|
28
|
-
- **Document**: the OpenAPI output of `Http.api`, produced by Effect (`OpenApi.fromApi`).
|
|
28
|
+
- **Document**: the OpenAPI output of `Http.api`, produced by Effect (`OpenApi.fromApi`) and served by `Http.openApi`.
|
|
29
|
+
- **Promise client**: `ActionHttpClient.promise(Http)`, the native `HttpApiClient` as one Promise method per HTTP action, for code that does not run Effects. It rejects with what the native client fails with.
|
|
29
30
|
- **Catalog**: the offline JSON description of groups produced by `ActionCatalog.make`, with standalone JSON Schemas for encoded values. Descriptive only.
|
|
30
31
|
- **Identity**: the per-request principal, provided by `Authentication.middleware` under an application-owned tag. Never provided at startup.
|
|
31
32
|
|
package/docs/README.md
CHANGED
|
@@ -9,29 +9,30 @@ Read the card for a module before writing code that uses it. Exported TypeScript
|
|
|
9
9
|
Package facts that apply everywhere:
|
|
10
10
|
|
|
11
11
|
- Every module is a subpath import: `import * as Action from "@gjermundgaraba/effect-actions/Action"`. There is no package root.
|
|
12
|
-
- The `effect` peer
|
|
12
|
+
- The `effect` peer accepts any 4.0 release candidate from `4.0.0-rc.116` on (`>=4.0.0-rc.116 <4.0.0`). The package is built and tested against `4.0.0-rc.117`.
|
|
13
13
|
- Contracts are pure values. Nothing runs, binds, or acquires services until an adapter layer is built.
|
|
14
14
|
|
|
15
15
|
## Pages
|
|
16
16
|
|
|
17
|
-
| Read
|
|
18
|
-
|
|
|
19
|
-
| [setup.md](setup.md)
|
|
20
|
-
| [Action.md](Action.md)
|
|
21
|
-
| [ActionGroup.md](ActionGroup.md)
|
|
22
|
-
| [ActionHttp.md](ActionHttp.md)
|
|
23
|
-
| [
|
|
24
|
-
| [
|
|
25
|
-
| [
|
|
26
|
-
| [
|
|
27
|
-
| [
|
|
28
|
-
| [
|
|
29
|
-
| [
|
|
30
|
-
| [
|
|
17
|
+
| Read | When you need to |
|
|
18
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
19
|
+
| [setup.md](setup.md) | install, pin versions, pick entry points, know what the package does not do |
|
|
20
|
+
| [Action.md](Action.md) | define one contract: input, success, errors, read/write access, HTTP and MCP flags |
|
|
21
|
+
| [ActionGroup.md](ActionGroup.md) | group contracts, declare shared errors, bind handlers, map every contract by name |
|
|
22
|
+
| [ActionHttp.md](ActionHttp.md) | serve JSON POST routes, declare surface errors and one hook, publish OpenAPI, call it |
|
|
23
|
+
| [ActionHttpClient.md](ActionHttpClient.md) | call the HTTP API with Promises, from code that does not run Effects |
|
|
24
|
+
| [ActionMcp.md](ActionMcp.md) | serve MCP 2026-07-28 tools over Streamable HTTP or stdio |
|
|
25
|
+
| [ActionToolkit.md](ActionToolkit.md) | use actions as a native Effect AI `Toolkit` without a server |
|
|
26
|
+
| [ActionCli.md](ActionCli.md) | run handlers in-process from a terminal command |
|
|
27
|
+
| [ActionCliClient.md](ActionCliClient.md) | call the HTTP API from a terminal command |
|
|
28
|
+
| [ActionCatalog.md](ActionCatalog.md) | export contracts as an offline JSON document |
|
|
29
|
+
| [Authentication.md](Authentication.md) | provide a per-request identity, publish RFC 9728 discovery, build bearer challenges |
|
|
30
|
+
| [Testing.md](Testing.md) | call routes and tools in memory, build MCP requests, drive the official MCP client |
|
|
31
|
+
| [guarantees.md](guarantees.md) | cross-cutting rules: dependency lifetimes, wire formats, spans, request context, scope |
|
|
31
32
|
|
|
32
33
|
## Choose an adapter
|
|
33
34
|
|
|
34
|
-
- Callers speak JSON over HTTP: `ActionHttp`. Clients use Effect's `HttpApiClient` on `Http.api
|
|
35
|
+
- Callers speak JSON over HTTP: `ActionHttp`. Clients use Effect's `HttpApiClient` on `Http.api`, or `ActionHttpClient.promise(Http)` where nothing runs Effects.
|
|
35
36
|
- Callers are MCP clients: `ActionMcp.layerHttp` for a hosted endpoint, `ActionMcp.layerStdio` for a subprocess.
|
|
36
37
|
- Callers are an Effect AI program in the same process: `ActionToolkit`.
|
|
37
38
|
- Callers are humans or scripts in a terminal, handlers run locally: `ActionCli`.
|
|
@@ -43,7 +44,6 @@ One implementation (`group.implement(...)`) feeds every adapter.
|
|
|
43
44
|
## Minimal program
|
|
44
45
|
|
|
45
46
|
```ts
|
|
46
|
-
import { McpProtocol } from "effect/unstable/ai";
|
|
47
47
|
import { Effect, Layer, Schema } from "effect";
|
|
48
48
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
49
49
|
import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
|
|
@@ -68,7 +68,6 @@ const app = Actions.implement({
|
|
|
68
68
|
export const routes = Layer.mergeAll(
|
|
69
69
|
Http.layer([app]),
|
|
70
70
|
ActionMcp.layerHttp([app], {
|
|
71
|
-
protocols: [McpProtocol.v2026_07_28],
|
|
72
71
|
name: "greetings",
|
|
73
72
|
version: "1.0.0",
|
|
74
73
|
path: "/mcp",
|
|
@@ -83,6 +82,7 @@ Serve `routes` with `HttpRouter.serve` and a platform server layer. Result: `POS
|
|
|
83
82
|
Repository directory `examples/` ([on GitHub](https://github.com/gjermundgaraba/effect-actions/tree/main/examples)):
|
|
84
83
|
|
|
85
84
|
- `quickstart.ts`, `quickstart-client.ts`: the minimal program and its typed client.
|
|
85
|
+
- `promise-client.ts`: the Promise client, with a declared error and an unreachable server.
|
|
86
86
|
- `contracts.ts`, `handlers.ts`, `app.ts`, `server.ts`: an authenticated application with three groups, per-group middleware, two MCP endpoints, OpenAPI and Swagger.
|
|
87
87
|
- `toolkit.ts`, `catalog.ts`, `cli.ts`, `cli-client.ts`, `mcp-stdio.ts`: one file per other projection.
|
|
88
88
|
|
package/docs/Testing.md
CHANGED
|
@@ -12,21 +12,23 @@ Import `@gjermundgaraba/effect-actions/Testing`; official-client helpers are in
|
|
|
12
12
|
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
13
13
|
| `Testing.httpClient(api, handler, options?)` | Native grouped `HttpApiClient` over an in-memory web handler; retains client middleware requirements. |
|
|
14
14
|
| `Testing.mcpRequest(options)` | Build a stateless MCP JSON-RPC `Request`. |
|
|
15
|
+
| `Testing.mcpCall(handler, options)` | Call one tool in memory and return its outcome without the wire envelope. |
|
|
15
16
|
| `TestingClient.withMcpClient(options, run)` | Connect the official client, run an asynchronous callback, then close it. |
|
|
16
17
|
|
|
17
18
|
`httpClient` takes native client options; `baseUrl` defaults to `http://localhost`.
|
|
18
19
|
Its exported `Handler` type is a web request to response Promise.
|
|
19
20
|
|
|
20
|
-
| MCP option | Meaning
|
|
21
|
-
| ------------------------------------- |
|
|
22
|
-
| `mcpRequest`: `url`, `method` | Required destination and JSON-RPC method.
|
|
23
|
-
| `mcpRequest`: `params`, `headers` | Optional parameters and request headers.
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `withMcpClient`: `
|
|
21
|
+
| MCP option | Meaning |
|
|
22
|
+
| ------------------------------------- | ----------------------------------------------------------- |
|
|
23
|
+
| `mcpRequest`: `url`, `method` | Required destination and JSON-RPC method. |
|
|
24
|
+
| `mcpRequest`: `params`, `headers` | Optional parameters and request headers. |
|
|
25
|
+
| `mcpCall`: `url`, `name` | Required endpoint URL and tool name (`mcp.name`). |
|
|
26
|
+
| `mcpCall`: `arguments`, `headers` | Optional tool arguments (default `{}`) and request headers. |
|
|
27
|
+
| `withMcpClient`: `fetch`, `path` | Required in-memory request handler and endpoint path. |
|
|
28
|
+
| `withMcpClient`: `baseUrl`, `headers` | Optional base URL (default `http://localhost`) and headers. |
|
|
27
29
|
|
|
28
|
-
Exported option/value types: `McpRequestOptions`, `McpRequestParams`, `McpRequestValue
|
|
29
|
-
from `Testing`, and `McpClientOptions` from `TestingClient`.
|
|
30
|
+
Exported option/value types: `McpRequestOptions`, `McpRequestParams`, `McpRequestValue`,
|
|
31
|
+
`McpCallOptions`, `McpCallResult` from `Testing`, and `McpClientOptions` from `TestingClient`.
|
|
30
32
|
|
|
31
33
|
## Canonical
|
|
32
34
|
|
|
@@ -53,13 +55,19 @@ try {
|
|
|
53
55
|
Testing.mcpRequest({ url: "http://localhost/mcp", method: "tools/list" }),
|
|
54
56
|
);
|
|
55
57
|
|
|
58
|
+
// One tool call, answered as `{ isError: false, value: "Hello, Ada!" }`.
|
|
59
|
+
const called = await Testing.mcpCall(web.handler, {
|
|
60
|
+
url: "http://localhost/mcp",
|
|
61
|
+
name: "greet",
|
|
62
|
+
arguments: { name: "Ada" },
|
|
63
|
+
});
|
|
64
|
+
|
|
56
65
|
// The official client uses the same in-memory handler.
|
|
57
|
-
const result = await TestingClient.withMcpClient(
|
|
58
|
-
{
|
|
59
|
-
(client) => client.callTool({ name: "greet", arguments: { name: "Ada" } }),
|
|
66
|
+
const result = await TestingClient.withMcpClient({ fetch: web.handler, path: "/mcp" }, (client) =>
|
|
67
|
+
client.callTool({ name: "greet", arguments: { name: "Ada" } }),
|
|
60
68
|
);
|
|
61
69
|
|
|
62
|
-
console.log({ greeting, listStatus: listed.status, result });
|
|
70
|
+
console.log({ greeting, listStatus: listed.status, called, result });
|
|
63
71
|
} finally {
|
|
64
72
|
await web.dispose();
|
|
65
73
|
}
|
|
@@ -70,14 +78,17 @@ try {
|
|
|
70
78
|
- `httpClient` is the same `HttpApiClient` consumers use, with its transport replaced by `handler`. Same `{ payload }` calls, same error channel.
|
|
71
79
|
- `mcpRequest` pins protocol version 2026-07-28 in both the header and `_meta`. Caller `params._meta` fields override the defaulted client capabilities and info; the merge is shallow. Application metadata is preserved.
|
|
72
80
|
- `params` accepts what `JSON.stringify` accepts, so tests can send malformed arguments on purpose. `undefined` fields are dropped.
|
|
73
|
-
- `
|
|
74
|
-
-
|
|
81
|
+
- `mcpCall` sends one `tools/call` through `mcpRequest` and resolves with `{ isError: false, value }`, the success taken from `structuredContent.value`, or `{ isError: true, error }`, the result's error text parsed as JSON. A declared error or a hook refusal is its JSON encoding, the same body HTTP sends; the native server's own messages (invalid arguments, defects) are not JSON and stay text. It reads a JSON or an event-stream response.
|
|
82
|
+
- `mcpCall` throws when the answer is not a tool result: a status other than 200 (an authentication refusal), or a JSON-RPC error (an unknown tool). Use `mcpRequest` to assert on those responses.
|
|
83
|
+
- `withMcpClient` pins the official client to MCP 2026-07-28, the only revision `ActionMcp` serves, and closes the transport in a `finally` block.
|
|
84
|
+
- Add `Authorization` through `headers` in the MCP helpers, or through `transformClient` in `httpClient` options.
|
|
75
85
|
- Direct handler tests can use `app.build` under `Effect.scoped`, but they bypass decoding, encoding, and middleware. Keep at least one adapter-level test per transport.
|
|
76
86
|
|
|
77
87
|
## Failure modes
|
|
78
88
|
|
|
79
89
|
- `@modelcontextprotocol/client` not found: only `TestingClient` needs it. Install the peer in devDependencies, or use `Testing.mcpRequest` instead.
|
|
80
|
-
-
|
|
90
|
+
- `withMcpClient` rejects its options with `versionNegotiation`: the revision is fixed at 2026-07-28. Delete the option.
|
|
91
|
+
- `MCP tools/call "<name>" answered 401`: the call reached authentication without a credential. Pass `headers: { authorization: "Bearer ..." }`.
|
|
81
92
|
- Handler leaks between tests: `web.dispose()` was not called. Register it with the test runner's cleanup hook.
|
|
82
93
|
- 404 from `httpClient`: the requested route was not mounted, or the path/base URL is wrong. Include `Http.layer([app])` in the served routes.
|
|
83
94
|
- Missing platform-service requirements when constructing the web handler: provide `HttpServer.layerServices` to the routes before `toWebHandler`.
|
package/docs/guarantees.md
CHANGED
|
@@ -32,7 +32,7 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
32
32
|
| Unknown path or method | 404 | n/a |
|
|
33
33
|
| Invalid JSON / content type | 400 / 415 | n/a |
|
|
34
34
|
|
|
35
|
-
- A group `schemaError` policy replaces the empty 400s on HTTP with a declared error and status. It never affects MCP. Details in [ActionGroup.md](ActionGroup.md).
|
|
35
|
+
- A group `schemaError` policy replaces the empty 400s on HTTP with a declared error and status: its `invalid` error for input that does not decode, its `internal` error for output that does not encode. It never affects MCP. Details in [ActionGroup.md](ActionGroup.md).
|
|
36
36
|
- `ActionHttp` sets no headers of its own. The host owns cache and challenge headers; `Authentication.middleware` marks its responses `cache-control: no-store`. Hook refusals and handler failures are ordinary declared error responses.
|
|
37
37
|
- Routes are `POST <apiPath>/<group>/<action>`. Operation IDs are `<group>.<action>`. Effect generates OpenAPI component names and references.
|
|
38
38
|
- `ActionHttp.make`'s `errors` are declared on every endpoint so clients decode the surface's own failures (401, 403, 429, 503). They are produced by middleware or by the binding's hook, never by a handler. `ActionMcp` and `ActionToolkit` declare their own `errors` the same way, joined into every tool's failure schema. Two errors may share an HTTP status; their `_tag`s must differ, because the client decodes a status by trying the schemas declared for it.
|
|
@@ -51,9 +51,9 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
51
51
|
|
|
52
52
|
## MCP transport
|
|
53
53
|
|
|
54
|
-
-
|
|
55
|
-
- `layerHttp` is single-endpoint Streamable HTTP, never two-endpoint HTTP+SSE
|
|
56
|
-
- Cancellation is Effect's native RPC interruption.
|
|
54
|
+
- MCP 2026-07-28 is the only revision served, on both transports; there is no option to select another. Effect owns version checks and rejects other revisions. Over HTTP the endpoint is stateless: no initialize handshake and no session.
|
|
55
|
+
- `layerHttp` is single-endpoint Streamable HTTP, never two-endpoint HTTP+SSE.
|
|
56
|
+
- Cancellation is Effect's native RPC interruption. Without an HTTP session, `notifications/cancelled` interrupts nothing over HTTP; a call ends with its request.
|
|
57
57
|
- Each endpoint or subprocess owns a fresh native tool registry. That isolates names, not application context.
|
|
58
58
|
|
|
59
59
|
## Scope
|
package/docs/setup.md
CHANGED
|
@@ -5,12 +5,12 @@ Install, version pins, entry points, and the boundaries of the package.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pnpm add @gjermundgaraba/effect-actions effect@4.0.0-rc.
|
|
8
|
+
pnpm add @gjermundgaraba/effect-actions effect@4.0.0-rc.117
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
| Need | Add |
|
|
12
12
|
| ------------------------------ | ----------------------------------------------------- |
|
|
13
|
-
| Node HTTP server or stdio host | `@effect/platform-node@4.0.0-rc.
|
|
13
|
+
| Node HTTP server or stdio host | `@effect/platform-node@4.0.0-rc.117` |
|
|
14
14
|
| `TestingClient.withMcpClient` | `@modelcontextprotocol/client@^2.0.0` (optional peer) |
|
|
15
15
|
|
|
16
16
|
Node `^22.12.0 || ^24.0.0 || >=26.0.0`. ESM only.
|
|
@@ -23,6 +23,7 @@ There is no package root. Import one module per subpath, as a namespace:
|
|
|
23
23
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
24
24
|
import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
|
|
25
25
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
26
|
+
import * as ActionHttpClient from "@gjermundgaraba/effect-actions/ActionHttpClient";
|
|
26
27
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
27
28
|
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
28
29
|
import * as ActionCatalog from "@gjermundgaraba/effect-actions/ActionCatalog";
|
|
@@ -39,11 +40,11 @@ and `TestingClient` is the only module that imports the optional `@modelcontextp
|
|
|
39
40
|
peer. Because there is no package root, importing a contract module never pulls in an MCP
|
|
40
41
|
server or the peer.
|
|
41
42
|
|
|
42
|
-
Companion Effect modules you will import alongside: `effect/unstable/httpapi` (`HttpApiClient`, `OpenApi`, `HttpApiSwagger`, `HttpApiScalar`), `effect/unstable/http` (`HttpRouter`, `HttpServerResponse`, `FetchHttpClient
|
|
43
|
+
Companion Effect modules you will import alongside: `effect/unstable/httpapi` (`HttpApiClient`, `OpenApi`, `HttpApiSwagger`, `HttpApiScalar`), `effect/unstable/http` (`HttpRouter`, `HttpServerResponse`, `FetchHttpClient`, `HttpClientError`), `effect/unstable/cli` (`Command`, `Flag`, `Argument`).
|
|
43
44
|
|
|
44
45
|
## Rules
|
|
45
46
|
|
|
46
|
-
-
|
|
47
|
+
- Install a single `effect` version within the peer range, and keep every `@effect/*` package on that same release candidate. Mixed rc versions fail at the type level in ways that look like library bugs.
|
|
47
48
|
- Never import from `dist/` paths or from a package root. Only the subpaths above are public.
|
|
48
49
|
- Schemas passed to `Action.make` must be service-free (`Schema.Codec<_, _, never, never>`). Handlers may require services.
|
|
49
50
|
|
|
@@ -52,12 +53,12 @@ Companion Effect modules you will import alongside: `effect/unstable/httpapi` (`
|
|
|
52
53
|
What the package does, and nothing else:
|
|
53
54
|
|
|
54
55
|
- HTTP means JSON `POST` endpoints built on Effect's `HttpApi`. It is not Effect RPC.
|
|
55
|
-
- MCP means one native `McpServer` `Tool` per action. There is no MCP SDK runtime dependency.
|
|
56
|
+
- MCP means one native `McpServer` `Tool` per action, over MCP 2026-07-28 only. There is no MCP SDK runtime dependency.
|
|
56
57
|
- Actions are unary: one decoded input, one decoded success or one declared error. No streaming, uploads, prompts, resources, retries, or code-execution sandbox.
|
|
57
58
|
- Authentication and authorization are the application's. The library provides the seams — `Authentication.middleware` for identity, one `before` hook per surface for the authorization rule, `access` for what that rule reads, each surface's `errors` for what a caller can decode — and no token verifier, error types or scopes of its own. Tool discovery is never filtered by actor.
|
|
58
59
|
|
|
59
60
|
## Failure modes
|
|
60
61
|
|
|
61
62
|
- `Cannot find module '@gjermundgaraba/effect-actions'`: there is no root export. Import a subpath.
|
|
62
|
-
- Type errors inside `effect/unstable/*` after install: `effect` version drift.
|
|
63
|
+
- Type errors inside `effect/unstable/*` after install: `effect` version drift. Install one `effect` release candidate within the peer range for every package.
|
|
63
64
|
- `@modelcontextprotocol/client` resolution errors in a server build: something imported `TestingClient`. Only tests should.
|