@gjermundgaraba/effect-actions 0.4.0 → 0.5.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 +10 -8
- package/dist/Action.mjs +1 -0
- package/dist/ActionCatalog.d.mts +2 -2
- package/dist/ActionCatalog.mjs +10 -9
- package/dist/ActionCli.d.mts +5 -3
- package/dist/ActionCli.mjs +3 -8
- package/dist/ActionCliClient.d.mts +9 -6
- package/dist/ActionCliClient.mjs +9 -11
- package/dist/ActionGroup.d.mts +3 -5
- package/dist/ActionHttp.d.mts +4 -5
- package/dist/ActionHttp.mjs +10 -33
- package/dist/ActionMcp.d.mts +6 -21
- package/dist/ActionMcp.mjs +4 -12
- package/dist/ActionToolkit.d.mts +5 -17
- package/dist/ActionToolkit.mjs +2 -7
- package/dist/internal/actions.mjs +11 -2
- package/dist/internal/cli.d.mts +2 -2
- package/dist/internal/cli.mjs +34 -20
- package/dist/internal/implementation.d.mts +17 -6
- package/dist/internal/implementation.mjs +9 -1
- package/dist/internal/tools.d.mts +22 -0
- package/dist/internal/tools.mjs +2 -17
- package/docs/Action.md +23 -55
- package/docs/ActionCatalog.md +32 -35
- package/docs/ActionCli.md +25 -52
- package/docs/ActionCliClient.md +16 -30
- package/docs/ActionGroup.md +16 -46
- package/docs/ActionHttp.md +27 -41
- package/docs/ActionMcp.md +115 -82
- package/docs/ActionToolkit.md +28 -46
- package/docs/Authentication.md +18 -36
- package/docs/CONTEXT.md +3 -3
- package/docs/README.md +13 -7
- package/docs/Testing.md +44 -52
- package/docs/guarantees.md +9 -9
- package/package.json +1 -1
package/docs/Authentication.md
CHANGED
|
@@ -2,46 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Two helpers for hosts that own identity: router middleware that provides a request-scoped
|
|
4
4
|
identity service, and RFC 9728 protected-resource discovery with bearer challenges. Token
|
|
5
|
-
verification, login, and consent stay in the application. Authorization
|
|
6
|
-
|
|
5
|
+
verification, login, and consent stay in the application. Authorization belongs in each adapter's
|
|
6
|
+
`before` hook ([guarantees.md](guarantees.md)), not in this module or the group.
|
|
7
7
|
|
|
8
8
|
## API
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
10
|
+
Import `@gjermundgaraba/effect-actions/Authentication`.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
handles: never;
|
|
20
|
-
error: never;
|
|
21
|
-
requires: Exclude<R, HttpRouter.Provided>;
|
|
22
|
-
layerError: never;
|
|
23
|
-
layerRequires: never;
|
|
24
|
-
}>;
|
|
25
|
-
|
|
26
|
-
const protectedResource: (options: ProtectedResourceOptions) => {
|
|
27
|
-
layer: Layer.Layer<never, never, HttpRouter.HttpRouter>; // GET/HEAD /.well-known/oauth-protected-resource<path>
|
|
28
|
-
metadataUrl: string;
|
|
29
|
-
challenge: (challenge?: BearerChallengeOptions) => string; // WWW-Authenticate value
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
interface ProtectedResourceOptions {
|
|
33
|
-
readonly resource: string; // exact OAuth resource identifier; its path and query select the discovery path
|
|
34
|
-
readonly authorizationServers: NonEmptyReadonlyArray<string>;
|
|
35
|
-
readonly scopesSupported?: ReadonlyArray<string>;
|
|
36
|
-
readonly resourceName?: string;
|
|
37
|
-
}
|
|
12
|
+
| API | Purpose |
|
|
13
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `middleware(service, authenticate)` | Native router middleware: provide an identity service per request, or return the response authentication fails with. |
|
|
15
|
+
| `protectedResource(options)` | Public RFC 9728 discovery routes, metadata URL and bearer challenge builder. |
|
|
16
|
+
| `discovery.layer`, `discovery.metadataUrl` | Router layer and public discovery URL. |
|
|
17
|
+
| `discovery.challenge(options?)` | Escaped `WWW-Authenticate` header value. |
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
19
|
+
`authenticate` is an Effect producing the identity or failing with `HttpServerResponse`.
|
|
20
|
+
Its dependencies remain request requirements. Provide the returned middleware's `.layer`
|
|
21
|
+
to routes requiring that identity.
|
|
22
|
+
|
|
23
|
+
| Option type | Fields |
|
|
24
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
25
|
+
| `ProtectedResourceOptions` | Required `resource` and nonempty `authorizationServers`; optional `scopesSupported`, `resourceName`. |
|
|
26
|
+
| `BearerChallengeOptions` | Optional `error` (`invalid_token` or `insufficient_scope`), `errorDescription`, and space-separated `scope`. |
|
|
45
27
|
|
|
46
28
|
## Canonical
|
|
47
29
|
|
|
@@ -88,7 +70,7 @@ export const authentication = Authentication.middleware(
|
|
|
88
70
|
|
|
89
71
|
export const routes = Layer.mergeAll(
|
|
90
72
|
discovery.layer, // public
|
|
91
|
-
Http.layer(UserApp).pipe(Layer.provide(authentication.layer)),
|
|
73
|
+
Http.layer([UserApp]).pipe(Layer.provide(authentication.layer)),
|
|
92
74
|
mcp.pipe(Layer.provide(authentication.layer)),
|
|
93
75
|
);
|
|
94
76
|
```
|
package/docs/CONTEXT.md
CHANGED
|
@@ -12,13 +12,13 @@ use these words with these meanings.
|
|
|
12
12
|
- **Handler**: a function from decoded input to an Effect of decoded success, failing only with the action's declared errors, possibly requiring services.
|
|
13
13
|
- **Implementation**: a nominal binding of a complete handler record to a group, made by `group.implement`. Carries build-time error and requirement channels. Adapters accept implementations; `ActionCatalog` accepts contracts.
|
|
14
14
|
- **Builder**: the record or Effect passed to `implement`. Runs once per adapter layer that serves the implementation.
|
|
15
|
-
- **Pre-handler hook**: the `before` function an adapter binds. Runs once
|
|
15
|
+
- **Pre-handler hook**: the `before` function an adapter binds. Runs once after successful input decoding, with the selected action contract, before its handler. Invalid input skips the hook. Fails with the surface's own errors. Its services are request-time requirements.
|
|
16
16
|
- **Build-time requirement**: a service yielded in the builder. Resolved when an adapter layer is built.
|
|
17
17
|
- **Request-time requirement**: a service yielded inside a handler. Supplied per invocation: by router middleware for HTTP-hosted adapters, by the host for Toolkit, CLI, and stdio.
|
|
18
18
|
- **Adapter** (also **projection**): a module that maps implementations or contracts onto one surface: `ActionHttp`, `ActionMcp`, `ActionToolkit`, `ActionCli`, `ActionCliClient`, `ActionCatalog`. Each adapter reads only what it serves and validates only its own namespace.
|
|
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
|
-
- **Declared error**: an error schema listed on an action or inherited from its group. Handlers may fail with it.
|
|
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
22
|
- **Schema-error policy**: a group option mapping Effect's `HttpApiSchemaError` (decode or encode failure) to a declared policy error with a status. 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.
|
|
@@ -26,7 +26,7 @@ use these words with these meanings.
|
|
|
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
28
|
- **Document**: the OpenAPI output of `Http.api`, produced by Effect (`OpenApi.fromApi`).
|
|
29
|
-
- **Catalog**: the offline JSON description of groups produced by `ActionCatalog.make
|
|
29
|
+
- **Catalog**: the offline JSON description of groups produced by `ActionCatalog.make`, with standalone JSON Schemas for encoded values. Descriptive only.
|
|
30
30
|
- **Identity**: the per-request principal, provided by `Authentication.middleware` under an application-owned tag. Never provided at startup.
|
|
31
31
|
|
|
32
32
|
## Seams
|
package/docs/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# effect-actions reference
|
|
2
2
|
|
|
3
3
|
Reference for `@gjermundgaraba/effect-actions`, written for coding agents. Every page is a
|
|
4
|
-
card with the same sections: **API** (
|
|
4
|
+
card with the same sections: **API** (inventory and options), **Canonical** (the one right way to write
|
|
5
5
|
it), **Rules** (must and never), **Failure modes** (what you see when it is wrong, and the fix).
|
|
6
|
-
Read the card for a module before writing code that uses it. Vocabulary is defined in
|
|
6
|
+
Read the card for a module before writing code that uses it. Exported TypeScript declarations are the exact signature reference. Vocabulary is defined in
|
|
7
7
|
[CONTEXT.md](CONTEXT.md); the pages use those terms exactly.
|
|
8
8
|
|
|
9
9
|
Package facts that apply everywhere:
|
|
@@ -66,11 +66,13 @@ const app = Actions.implement({
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
export const routes = Layer.mergeAll(
|
|
69
|
-
Http.layer(
|
|
70
|
-
ActionMcp.layerHttp(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
Http.layer([app]),
|
|
70
|
+
ActionMcp.layerHttp([app], {
|
|
71
|
+
protocols: [McpProtocol.v2026_07_28],
|
|
72
|
+
name: "greetings",
|
|
73
|
+
version: "1.0.0",
|
|
74
|
+
path: "/mcp",
|
|
75
|
+
}),
|
|
74
76
|
);
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -83,3 +85,7 @@ Repository directory `examples/` ([on GitHub](https://github.com/gjermundgaraba/
|
|
|
83
85
|
- `quickstart.ts`, `quickstart-client.ts`: the minimal program and its typed client.
|
|
84
86
|
- `contracts.ts`, `handlers.ts`, `app.ts`, `server.ts`: an authenticated application with three groups, per-group middleware, two MCP endpoints, OpenAPI and Swagger.
|
|
85
87
|
- `toolkit.ts`, `catalog.ts`, `cli.ts`, `cli-client.ts`, `mcp-stdio.ts`: one file per other projection.
|
|
88
|
+
|
|
89
|
+
- `mcp-browser.ts`: a stateless MCP endpoint with a separate browser CORS policy.
|
|
90
|
+
- `toolkit-authorized.ts`: native Toolkit invocation with authorization and correctly scoped identity.
|
|
91
|
+
- `testing.ts`: in-memory HTTP and MCP calls with cleanup.
|
package/docs/Testing.md
CHANGED
|
@@ -5,43 +5,28 @@ loads the optional `@modelcontextprotocol/client` peer and nothing else does.
|
|
|
5
5
|
|
|
6
6
|
## API
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
|
|
8
|
+
Import `@gjermundgaraba/effect-actions/Testing`; official-client helpers are in the separate
|
|
9
|
+
`@gjermundgaraba/effect-actions/TestingClient` subpath.
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
| API | Purpose |
|
|
12
|
+
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `Testing.httpClient(api, handler, options?)` | Native grouped `HttpApiClient` over an in-memory web handler; retains client middleware requirements. |
|
|
14
|
+
| `Testing.mcpRequest(options)` | Build a stateless MCP JSON-RPC `Request`. |
|
|
15
|
+
| `TestingClient.withMcpClient(options, run)` | Connect the official client, run an asynchronous callback, then close it. |
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
api: Api,
|
|
17
|
-
handler: Handler,
|
|
18
|
-
options?: Parameters<typeof HttpApiClient.make>[1],
|
|
19
|
-
) => Effect.Effect<HttpApiClient.Client<Api>>;
|
|
17
|
+
`httpClient` takes native client options; `baseUrl` defaults to `http://localhost`.
|
|
18
|
+
Its exported `Handler` type is a web request to response Promise.
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
| `withMcpClient`: `fetch`, `path` | Required in-memory request handler and endpoint path. |
|
|
25
|
+
| `withMcpClient`: `baseUrl`, `headers` | Optional base URL (default `http://localhost`) and headers. |
|
|
26
|
+
| `withMcpClient`: `versionNegotiation` | Optional native client negotiation configuration; omitted means its native default. |
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
readonly method: string; // "tools/list", "tools/call", ...
|
|
27
|
-
readonly params?: McpRequestParams; // anything JSON.stringify accepts, undefined fields included
|
|
28
|
-
readonly headers?: HeadersInit;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Connect the official client, run the callback, always close the transport. */
|
|
32
|
-
const withMcpClient: <A>(
|
|
33
|
-
options: McpClientOptions,
|
|
34
|
-
run: (client: Client) => Promise<A>,
|
|
35
|
-
) => Promise<A>;
|
|
36
|
-
|
|
37
|
-
interface McpClientOptions {
|
|
38
|
-
readonly fetch: (request: Request) => Promise<Response>; // the web handler
|
|
39
|
-
readonly path: string;
|
|
40
|
-
readonly versionNegotiation?: ClientOptions["versionNegotiation"]; // official client default is legacy
|
|
41
|
-
readonly baseUrl?: string | URL; // http://localhost
|
|
42
|
-
readonly headers?: HeadersInit;
|
|
43
|
-
}
|
|
44
|
-
```
|
|
28
|
+
Exported option/value types: `McpRequestOptions`, `McpRequestParams`, `McpRequestValue`
|
|
29
|
+
from `Testing`, and `McpClientOptions` from `TestingClient`.
|
|
45
30
|
|
|
46
31
|
## Canonical
|
|
47
32
|
|
|
@@ -53,25 +38,31 @@ import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
|
|
|
53
38
|
import { Http, routes } from "./quickstart.js";
|
|
54
39
|
|
|
55
40
|
const web = HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)));
|
|
56
|
-
// call web.dispose() when the test finishes
|
|
57
|
-
|
|
58
|
-
// HTTP, typed, in memory.
|
|
59
|
-
const greeting = Effect.gen(function* () {
|
|
60
|
-
const client = yield* Testing.httpClient(Http.api, web.handler);
|
|
61
|
-
|
|
62
|
-
return yield* client.greetings.greet({ payload: { name: "Ada" } });
|
|
63
|
-
});
|
|
64
41
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
42
|
+
try {
|
|
43
|
+
const greeting = await Effect.runPromise(
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
const client = yield* Testing.httpClient(Http.api, web.handler);
|
|
46
|
+
|
|
47
|
+
return yield* client.greetings.greet({ payload: { name: "Ada" } });
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Raw MCP request; this stateless revision needs no initialize handshake.
|
|
52
|
+
const listed = await web.handler(
|
|
53
|
+
Testing.mcpRequest({ url: "http://localhost/mcp", method: "tools/list" }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// The official client uses the same in-memory handler.
|
|
57
|
+
const result = await TestingClient.withMcpClient(
|
|
58
|
+
{ fetch: web.handler, path: "/mcp", versionNegotiation: { mode: { pin: "2026-07-28" } } },
|
|
59
|
+
(client) => client.callTool({ name: "greet", arguments: { name: "Ada" } }),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log({ greeting, listStatus: listed.status, result });
|
|
63
|
+
} finally {
|
|
64
|
+
await web.dispose();
|
|
65
|
+
}
|
|
75
66
|
```
|
|
76
67
|
|
|
77
68
|
## Rules
|
|
@@ -88,4 +79,5 @@ const result = await TestingClient.withMcpClient(
|
|
|
88
79
|
- `@modelcontextprotocol/client` not found: only `TestingClient` needs it. Install the peer in devDependencies, or use `Testing.mcpRequest` instead.
|
|
89
80
|
- Official client fails negotiation against a stateless endpoint: set `versionNegotiation: { mode: { pin: "2026-07-28" } }`.
|
|
90
81
|
- Handler leaks between tests: `web.dispose()` was not called. Register it with the test runner's cleanup hook.
|
|
91
|
-
- 404 from `httpClient`:
|
|
82
|
+
- 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
|
+
- Missing platform-service requirements when constructing the web handler: provide `HttpServer.layerServices` to the routes before `toWebHandler`.
|
package/docs/guarantees.md
CHANGED
|
@@ -5,17 +5,17 @@ Rules that hold across every adapter. Module pages link here rather than repeati
|
|
|
5
5
|
## Dependency lifetimes
|
|
6
6
|
|
|
7
7
|
- Handlers receive decoded input, return decoded results, and may fail only with declared errors. Anything else is a defect.
|
|
8
|
-
- Each surface binds its own pre-handler hook: `Http.layer({ before }
|
|
8
|
+
- Each surface binds its own pre-handler hook: `Http.layer(apps, { before })`, `ActionMcp.layerHttp`/`layerStdio`, `ActionToolkit.make`, `ActionCli.command`/`group`. After successful input decoding, a bound hook runs once before the selected handler and outside its span. There is no per-action opt-out; direct `build` calls bypass dispatch.
|
|
9
9
|
- The hook receives the selected action contract (`Action.Any`), so a policy reads `action.access`, `action.name` or `action.mcp` instead of a hand-maintained list. It is not authentication: establish identity in middleware, then let the hook authorize what that identity may do.
|
|
10
|
-
- It fails with the surface's own `errors`, the same list that declares what the surface answers instead of a handler, so a refusal is encoded exactly like a declared error: the schema's `httpApiStatus` on HTTP, an `isError` tool result on MCP, a returned failure on the Toolkit. `ActionCli` declares no errors because it
|
|
10
|
+
- It fails with the surface's own `errors`, the same list that declares what the surface answers instead of a handler, so a refusal is encoded exactly like a declared error: the schema's `httpApiStatus` on HTTP, an `isError` tool result on MCP, a returned failure on the Toolkit. `ActionCli` declares no surface errors because it does not serialize failures; there a refusal is a typed failure of the command effect.
|
|
11
11
|
- Its services are request-time requirements, like a handler's, and they join the adapter's request context. A local caller (`ActionCli`, `ActionToolkit`, stdio) supplies them itself.
|
|
12
|
-
-
|
|
12
|
+
- All surfaces decode input before dispatch. Invalid input skips the hook and handler, so unauthorized callers can receive schema errors. Use outer native HTTP middleware for admission that must run before decoding; a pre-handler hook does not provide that guarantee.
|
|
13
13
|
- Build-time services are those yielded in the `implement` builder Effect. They resolve once per adapter layer, in that layer's scope. An implementation served by two adapters is built twice. Share state through a Layer provided to the adapters, never through the builder.
|
|
14
14
|
- Request-time services are those yielded inside a handler. For HTTP-hosted adapters (`ActionHttp`, `ActionMcp.layerHttp`) they appear as `HttpRouter.Request.From<"Requires", R>` and are supplied by router middleware, `HttpRouter.provideRequest`, or request context. For `ActionToolkit`, `ActionCli`, and `ActionMcp.layerStdio` the host supplies them at invocation.
|
|
15
15
|
- Use distinct tags for build-time capabilities and request-scoped identity. Never provide an identity or tenant tag in a startup layer or root context. The adapters use native Effect context capture and merging: a startup value under a tag can shadow a request value or satisfy a missing one. Types verify presence, not provenance.
|
|
16
|
-
- Authentication must establish identity on every protected request. Types cannot verify that a handler performed authorization; the `before` hook is the one place the library guarantees runs
|
|
17
|
-
- `access` is contract metadata, required on every action and kept as a literal type.
|
|
18
|
-
- `ActionCli` acquires the implementation per invocation and releases it after the call. `ActionToolkit.layer` acquires it once for the layer's lifetime.
|
|
16
|
+
- Authentication must establish identity on every protected request. Types cannot verify that a handler performed authorization; the `before` hook is the one place the library guarantees runs before the handler, so put the check there rather than in each handler.
|
|
17
|
+
- `access` is contract metadata, required on every action and kept as a literal type. It supplies the default MCP `readOnly` hint and the `action.access` span/log annotation, but enforces no authorization. A hook can switch on what an action does instead of on its name. The catalog omits it.
|
|
18
|
+
- `ActionCli` acquires the implementation per invocation and releases it after the call. `ActionToolkit.make(...).layer` acquires it once for the layer's lifetime.
|
|
19
19
|
|
|
20
20
|
## Wire behavior
|
|
21
21
|
|
|
@@ -26,14 +26,14 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
26
26
|
| Input | decoded with the schema's JSON codec; failure is an empty **400** | decoded with the schema's JSON codec; failure is `InvalidParams`, presented as an `isError` result |
|
|
27
27
|
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: { value: <encoded> }` |
|
|
28
28
|
| Declared error | encoded by its schema with its `httpApiStatus` (unannotated: 500): `{"_tag":"UserNotFound","id":"x"}` | `isError: true`; text content is the same JSON; no `structuredContent` |
|
|
29
|
-
| Pre-handler hook failure | the same, using the hook's error schema,
|
|
29
|
+
| Pre-handler hook failure | the same, using the hook's error schema, after input decoding; the handler never runs | the same `isError` result, after arguments are decoded; the handler never runs |
|
|
30
30
|
| Invalid output encoding | empty **400** (`HttpApiSchemaError`) | `isError: true`, text `Tool execution failed due to an internal server error.`; cause logged, not sent |
|
|
31
31
|
| Defect | empty **500** | same generic `isError` result; cause logged, not sent |
|
|
32
32
|
| Unknown path or method | 404 | n/a |
|
|
33
33
|
| Invalid JSON / content type | 400 / 415 | n/a |
|
|
34
34
|
|
|
35
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).
|
|
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.
|
|
39
39
|
- HTTP strips undeclared input fields. MCP tools are strict (`Tool.Strict`): undeclared arguments are an invalid-arguments result and input schemas publish `additionalProperties: false`. HTTP is not strict, and cannot be made strict without a hack: Effect merges one `HttpApi.ParseOptions` per endpoint and uses it for payload decoding _and_ error encoding, so `onExcessProperty: "error"` also rejects a `TaggedError` instance's own `message` and `stack`, turning a declared 409 into an empty 500. Making the payload schema itself strict would require wrapping it in an open schema, which erases its OpenAPI shape. Declare the fields you accept and treat extra HTTP fields as ignored.
|
|
@@ -46,7 +46,7 @@ Defaults without a schema-error policy. HTTP is a native `HttpApi`; MCP is a nat
|
|
|
46
46
|
|
|
47
47
|
## Observability
|
|
48
48
|
|
|
49
|
-
- Each handler runs in a span named `<group>.<action>`, a child of the transport's request span. The pre-handler hook, decoding and encoding happen outside the handler span.
|
|
49
|
+
- Each handler runs in a span named `<group>.<action>`, a child of the transport's request span, with attributes `action.group`, `action.name` and `action.access`. Every log line the handler writes carries the same three annotations. The pre-handler hook, decoding and encoding happen outside the handler span.
|
|
50
50
|
- MCP defects and encoding failures are logged with their cause and answered generically.
|
|
51
51
|
|
|
52
52
|
## MCP transport
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Define Effect action contracts once and project them onto HTTP, MCP, native Toolkits, catalogs, and CLIs.",
|
|
5
5
|
"homepage": "https://github.com/gjermundgaraba/effect-actions#readme",
|
|
6
6
|
"bugs": {
|