@gjermundgaraba/effect-actions 0.8.0 → 0.10.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 +57 -50
- package/dist/Action.d.mts +285 -79
- package/dist/Action.mjs +124 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +108 -81
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +82 -57
- package/dist/Testing.mjs +89 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +11 -0
- package/dist/internal/mcp.mjs +52 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +401 -73
- package/docs/ActionCli.md +280 -51
- package/docs/ActionHttp.md +266 -82
- package/docs/ActionMcp.md +149 -104
- package/docs/ActionToolkit.md +97 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +23 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +81 -43
- package/docs/setup.md +90 -27
- package/package.json +9 -17
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
package/docs/guarantees.md
CHANGED
|
@@ -1,65 +1,103 @@
|
|
|
1
1
|
# Guarantees
|
|
2
2
|
|
|
3
|
-
Rules that hold across every
|
|
3
|
+
Rules that hold across every surface. Module pages state what is their own and link here for the rest.
|
|
4
4
|
|
|
5
5
|
## Dependency lifetimes
|
|
6
6
|
|
|
7
|
-
- Handlers receive decoded input, return decoded results, and may fail only with declared errors.
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Request-time services are those yielded inside a handler. For HTTP-hosted
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
- Handlers receive decoded input, return decoded results, and may fail only with their action's declared errors and the built-in ones. A failure the types let through anyway, such as from plain JavaScript or a cast, is answered as the [wire table](#wire-behavior) says for an undeclared failure.
|
|
8
|
+
- Build-time services are those yielded in the builder Effect passed to `implement`, in an Effect building its authorizer and in a verifier's `Authentication.layer`. Each is a layer Effect memoizes. Within one layer graph it runs once, however many surfaces serve its implementation (HTTP layers, MCP endpoints, a Toolkit) or list its actions, and its scope is the host's. Two `implement` calls are two builders, even of one builder Effect: each runs once per graph.
|
|
9
|
+
- `HttpRouter.serve` and `Testing.layer` each build their routes in a layer graph of their own, which reuses what the host has already built but shares nothing it builds itself: a builder's run, a built authorizer, a service of your own such as an in-memory store. `HttpRouter.provideRequest` also builds the layer it provides in a graph of its own, which reuses what the routes' graph has already built. So one rule places builders and your own services alike: **with one server, put everything the process runs in the layer it serves**, a job or an agent beside the routes included; **otherwise, provide `Action.layer(implementations)` and the services they share above the server and its siblings**, such as a second server, a program running beside it, or several `Testing.layer`s. A sibling given neither builds a builder, and a service whose layer both provide, a second time, with state of its own, whenever the server's graph builds it first: `Layer.mergeAll` builds its layers concurrently, so whichever build reaches the memo map first decides, not the order it lists them in.
|
|
10
|
+
- `Action.layer` builds each builder first, once for every surface and client below it, with the services it is given. Building it acquires every supplied implementation's builder, including implementations no surface serves. Without it, a builder runs only for a surface serving its implementation, or a client of it.
|
|
11
|
+
- A builder runs again only in a separately built layer graph, such as a second `Testing.layer`. A local `ActionCli` command is the exception: it builds the selected implementation, its authorizer included, per invocation, even where the host already built it, and releases it after the call, once the handler's and the authorizer's own finalizers have run.
|
|
12
|
+
- Acquiring an `Action.client` builds its implementations into the layer graph it is acquired in, as a layer of it: in a builder or a layer, the host's graph, where it shares each builder's one run with every surface of that graph, whichever builds first; in a program, the graph of a layer provided around it, which every client acquired there shares; under none, a graph of its own. What it builds stays until its scope closes and nothing else in the graph holds it. Acquired per request, in a handler, it builds for every call unless a surface of the graph serves the same implementation, and its builders' startup services become that handler's request requirements: acquire it where builders live.
|
|
13
|
+
- **Without `Action.layer`, the builder runs with whichever surface's services Effect builds it with first.** Services provided around one surface reach the others too, and so does an `Action.layer` provided to one surface alone: provide its startup services once, to `Action.layer` above every surface that serves it. To give an HTTP or MCP surface a build and startup services of its own, wrap it in `Layer.fresh`, which builds everything inside it anew, and provide what the surfaces share, authentication included, outside it. A Toolkit's tools run on one build of their implementation, whatever layers of it are provided ([ActionToolkit.md](ActionToolkit.md#rules)): separate services take separate implementations.
|
|
14
|
+
- Request-time services are those yielded inside a handler or the authorizer itself. For HTTP-hosted surfaces (`ActionHttp`, `ActionMcp.layerHttp`) they appear as `HttpRouter.Request<"Requires", X>`, one for each service `X`, and are supplied by router middleware around the surface, `HttpRouter.provideRequest`, or request context. A protected action's identity is the exception: only its authentication provider supplies it, so the layer requires that provider instead, and nothing else provided stands in for it. For `ActionToolkit`, `ActionCli`, `ActionMcp.runStdio` and `Action.client` the host supplies them at invocation, the identity of every protected action included, whether or not anything reads it; a client's call reads them from its caller's context alone, never from what the client was acquired with.
|
|
15
|
+
- Each call has a scope of its own, on every surface: what the authorizer and the handler acquire is released when the call ends, the handler's first, before its result is encoded, and over HTTP before the route middleware resumes. A Toolkit call is no exception, whether `tools.handle` makes it or `LanguageModel` runs it, approved or not, and neither is an `Action.client` call. `Scope` is never a request-time requirement. A resource that must outlive a call is the builder's, or a service the host provides around its calls.
|
|
16
|
+
- Use distinct tags for build-time capabilities and request-scoped identity. Everything in a call's context wins over what the surface was built with under the same tag: what authentication, other router middleware, `HttpRouter.provideRequest` or a Toolkit's caller provides, and what the server runs in, as provided around `HttpRouter.serve`, `Testing.layer` or the program. A value provided to one surface's layer only fills in a service or reference the call lacks; to give one surface's requests their own, such as a log level or an `ErrorReporter`, use `HttpRouter.provideRequest` on its layer. So never provide an identity or tenant tag in a server's startup layer or root context: a protected route still requires its authentication provider, and a public handler or a native route reading the tag would serve every caller as that identity. In memory, a test serves protected routes behind an authentication provider, the real one or a test verifier of the same descriptor, and each client sends its own caller's credential ([Testing.md](Testing.md#signed-in-callers)). Types verify presence, not provenance, for every service but a protected action's identity.
|
|
17
|
+
|
|
18
|
+
## Authorization
|
|
19
|
+
|
|
20
|
+
- A contract states who may call it, `caller`: `Action.Anyone`, or the identity service a caller must have. Every surface enforces it, whatever the handler and the authorizer read. A remote surface, `ActionHttp.layer` or `ActionMcp.layerHttp`, names the contracts' authentication descriptor (`authentication: Login`), and its layer requires that descriptor's provider, `Authentication.layer`, which verifies each protected request's credential and provides the identity. A local surface requires the identity per call, in the types, and a call that lacks it anyway, as plain JavaScript may, fails with `Unauthenticated` before the authorizer runs. No surface takes an authorization policy of its own, so none can leave one out or loosen it.
|
|
21
|
+
- A call runs, in order: authentication, then over HTTP the layer's own `middleware` ([ActionHttp.md](ActionHttp.md#rules)), then input decoding, then the authorizer, for a protected action only, then the handler. Each step refuses before the next runs. Over HTTP and MCP a protected request is authenticated before its body is read: without a credential, a protected route answers 401 before a 415 or a 400, and a protected tool's call 401 before its arguments are decoded. On a local surface, the identity is checked after decoding.
|
|
22
|
+
- An implementation of protected actions states its authorizer, `Action.implement(actions, handlers, { authorize })`: leaving it out is a type error, and plain JavaScript leaving it out throws at `implement`. `Action.allowAll` lets every authenticated caller call. An implementation of public actions alone takes none, a type error too, and plain JavaScript passing one throws at `implement`. The authorizer never runs for a public action, even one implemented beside protected ones.
|
|
23
|
+
- The authorizer runs once per call, after input decodes, before the handler and outside its span. It runs on `ActionHttp`, `ActionMcp` over HTTP and stdio, `ActionToolkit`, a local `ActionCli` command, and `Action.client`. There is no per-surface override: a caller trusted on one surface, such as an admin CLI's operator, is an identity the host supplies there, which the same authorizer admits ([ActionCli.md](ActionCli.md#trusted-callers)). Calling a handler function directly bypasses dispatch, so call it through `Action.client`.
|
|
24
|
+
- The authorizer receives the selected action contract, typed as the implementation's own, so a policy reads `action.readOnly` or `action.name` instead of a hand-maintained list. It is not authentication: authentication establishes identity, then the authorizer decides what that identity may do. It fails only with a refusal, `Action.Unauthenticated` or `Action.Forbidden`: any other failure is a type error, and a limit on callers is the handler's. HTTP sends a refusal's JSON with 401 or 403, MCP an `isError` tool result, the Toolkit a returned failure, a CLI command the same JSON on stderr. A step-up refusal on a remote surface is the one exception, below.
|
|
25
|
+
- The services the authorizer yields are request-time requirements, like a handler's, and they join the surface's request context. A local caller (`ActionCli`, `ActionToolkit`, stdio, `Action.client`) supplies them itself. What an Effect building the authorizer yields: [dependency lifetimes](#dependency-lifetimes).
|
|
26
|
+
- A CLI command fails with Effect CLI's `UserError`, whose `cause` is what the authorizer or the handler fails with. Every local command may fail with the action's errors and `Action.BuiltIn` ([ActionCli.md](ActionCli.md#rules)).
|
|
27
|
+
- A step-up refusal is one an OAuth client acts on: `Unauthenticated`, or a `Forbidden` naming `scopes`. Step-up is Bearer's and protected actions': on a protected route of `ActionHttp`, or a protected tool's call on an `ActionMcp.layerHttp` endpoint naming a Bearer authentication descriptor, it answers the request with its HTTP status, 401 or 403, its JSON and its challenge, whether authentication, the authorizer or a handler fails with it. Over HTTP it answers the refusal as it leaves the layer's `middleware`: one that recovers from it keeps its own response, and one that turns it into another error the binding declares answers with that error. A public route's or a public tool's refusal never steps up, signed in or not: over HTTP its JSON with its status, over MCP an `isError` tool result.
|
|
28
|
+
- Over MCP that answer replaces the tool result, as MCP authorization defines, unless a handler's notification already started the response ([ActionMcp.md](ActionMcp.md#rules)). On an endpoint without authentication, or naming a descriptor of another scheme, there is no OAuth client to step up: it is answered like a declared error, an `isError` tool result the model reads, and over HTTP its JSON with its status, without an `insufficient_scope` challenge. A refusal of the authentication itself is still its 401 or 403.
|
|
29
|
+
- A `Forbidden` naming scopes carries the `insufficient_scope` challenge an OAuth client re-authorizes on ([Authentication.md](Authentication.md#rules)). A `Forbidden` naming no scopes is always answered like a declared error.
|
|
30
|
+
- Map another service's refusals before failing with them. A handler calling another service through a client that decodes the built-in errors, such as `ActionHttp.client`, gets that service's `Unauthenticated` or `Forbidden` as a value any handler may fail with; passed on as it is, it is this server's own refusal. On a remote surface it steps up: a caller whose token is valid is sent to sign in again, its 401 naming `invalid_token`, or to re-authorize for the other service's scopes. Map it to an error the action declares, or to a defect.
|
|
31
|
+
- Input decodes before the authorizer. Invalid input skips it and the handler, so a caller the authorizer would refuse can receive schema errors (HTTP's `InvalidInput` message describes the input's schema), as can any caller of a public action. To refuse a caller before decoding, over HTTP only, use the layer's `middleware`, inside authentication, or router middleware around the surface: [ActionHttp.md](ActionHttp.md#rules).
|
|
32
|
+
- A limit on callers goes in one of three places. After decoding, on every surface: the handler, failing with an error its action declares, the limiter a startup service its builder yields, so every surface of the graph counts against one ([Action.md](Action.md#implementations)). Before decoding, over HTTP only: the layer's own `middleware`, which runs after authentication and so may key the limit by the caller, or router middleware around the routes, which runs before authentication; either answers with an error of the binding's `error` ([ActionHttp.md](ActionHttp.md#binding-errors)). Input that does not decode never reaches a handler, so only middleware counts it, and a protected request without a valid credential reaches only router middleware.
|
|
33
|
+
- Types cannot verify that a verifier checks what it should, or that an authorizer states the right rule: the authorizer is the one place the library guarantees runs before the handler of a protected action, so put action-level authorization there. A check on one record, such as whether this actor may rename this user, needs the input and its data: make it in the handler's data access.
|
|
34
|
+
- `readOnly` is contract metadata, required on every action and kept as a literal type. It is a tool's read-only hint, a read-only tool's default `destructiveHint` and the `action.read_only` span/log annotation, but enforces no authorization. An authorizer can switch on what an action does instead of on its name.
|
|
19
35
|
|
|
20
36
|
## Wire behavior
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
HTTP is a native `HttpApi`; MCP is a native `McpServer` with one `Tool` per action. The
|
|
39
|
+
built-in errors are `Action.InvalidInput` (400), `Action.Unauthenticated` (401) and
|
|
40
|
+
`Action.Forbidden` (403), each `{ _tag, message }`; an `InvalidInput` also lists `issues`, empty
|
|
41
|
+
when it names none, and a `Forbidden` may add `scopes`.
|
|
23
42
|
|
|
24
|
-
| Case | HTTP | MCP
|
|
25
|
-
| --------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| Input | decoded with the schema's JSON codec; failure is
|
|
27
|
-
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: <encoded
|
|
28
|
-
| Declared error | encoded by its schema with its `httpApiStatus` (unannotated:
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
43
|
+
| Case | HTTP | MCP |
|
|
44
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| Input | decoded with the schema's JSON codec; failure is **400** `InvalidInput`, `message` the schema issues | decoded with the schema's JSON codec; failure is an `isError` result from 2025-11-25 on, and a JSON-RPC `InvalidParams` error before |
|
|
46
|
+
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: <encoded>` and its JSON as text on 2026-07-28; earlier stdio revisions structure only an object ([ActionMcp.md](ActionMcp.md#rules)) |
|
|
47
|
+
| Declared error | encoded by its schema with its `httpApiStatus` (unannotated: 422): `{"_tag":"UserNotFound","id":"x"}` | `isError: true`; text content is the same JSON; no `structuredContent` |
|
|
48
|
+
| Authentication | a protected route without a valid credential: **401** with a challenge, before its body is read | a request a protected tool's call or a native feature needs: **401** with a challenge, before its body is read ([ActionMcp.md](ActionMcp.md#rules)) |
|
|
49
|
+
| Authorizer refusal | its JSON with its status, **401** / **403**, after input decoding; the handler never runs | the same `isError` result, after arguments are decoded; on a Bearer-authenticated endpoint a protected tool's step-up refusal is **401** / **403** |
|
|
50
|
+
| Invalid output encoding | a defect: empty **500** | `isError: true`, text `Tool execution failed due to an internal server error.`; cause logged, not sent |
|
|
51
|
+
| Defect | empty **500** | same generic `isError` result; cause logged, not sent |
|
|
52
|
+
| Unknown path or method | 404 | another path: 404, as HTTP; another HTTP method on the endpoint: an empty **405** with `Allow: POST`; an unknown tool: a JSON-RPC `-32602` error, with 200 over HTTP |
|
|
53
|
+
| Invalid JSON / content type | 400 `InvalidInput` / 415 for a missing or non-JSON type, after authentication on a protected route | over HTTP, a JSON-RPC `Parse error` (`-32700`) with 200 / an empty **415** for a missing or non-JSON type; over stdio, a line that is not JSON goes unanswered |
|
|
34
54
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- Routes are `POST <
|
|
39
|
-
-
|
|
40
|
-
-
|
|
55
|
+
- The HTTP surfaces set no header of their own; handler failures are ordinary declared error responses. On a route a Bearer descriptor authenticates, a `Forbidden` naming scopes carries `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`.
|
|
56
|
+
- Authentication marks every response to a request it authenticates, on a protected HTTP route, an MCP endpoint naming a descriptor or a route of the host's own under `Authentication.protect`, `Cache-Control: no-store`, unless its route states its own caching, such as `private, max-age=31536000, immutable` for a content-addressed download. A covered route stating its own says `private` unless its answer is the same for every caller: a credential a proxy forwards is invisible to a cache in front of that proxy, which may store the answer and serve it to anyone. A failure that enclosing middleware serializes always gets `no-store`, whatever caching it states. An MCP endpoint's answer to a request it does not authenticate, discovery, its listing or a public tool's call without a credential, gets none, as a public HTTP route's does: it reads no credential. It gives every 401 among them without a challenge its scheme's, where the scheme has one, whether authentication, the authorizer or a handler answers it, a failure that enclosing middleware serializes included. As a protected resource, it names its metadata URL in every challenge, and a 401's names `scopesRequired`, and `invalid_token` when the request presented credentials ([Authentication.md](Authentication.md#rules)).
|
|
57
|
+
- A 401 carries a challenge only on a route authentication covers, and only where its scheme has one: Bearer's, an `Http` scheme naming itself, or Basic's with its realm, the descriptor's name. An API key's 401 is the same JSON and `Cache-Control: no-store`, without a challenge ([Authentication.md](Authentication.md#rules)).
|
|
58
|
+
- Routes are `POST <prefix>/<action>`; the prefix defaults to `/api`. Operation IDs are the action names. Effect generates OpenAPI component names and references.
|
|
59
|
+
- Effect reads each request body whole, with no size limit unless the host sets one: provide `Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))` to `HttpRouter.serve`'s layer. The limit covers every route on the server, MCP endpoints and the host's own routes included, except a body a route reads as a stream. Node's server (`NodeHttpServer`) closes the connection of a request over it without a response. `HttpRouter.toWebHandler` ignores it: limit bodies in the platform serving the handler.
|
|
60
|
+
- Every HTTP endpoint and every tool declares `InvalidInput`, `Unauthenticated` and `Forbidden` beyond the action's own errors, and an endpoint its binding's too, so typed callers decode them. Any handler may fail with them; no action or binding lists them, and no error of its own may reuse their tags.
|
|
61
|
+
- They are produced by decoding, by authentication, by an authorizer, or by any handler. A client decodes a status by trying the schemas declared for it, as it decodes any union: errors sharing a `_tag` are told apart by their other fields, the first that decodes winning, so make them disjoint, by a literal field for one. A built-in error is always tried first, so a loose schema of an action's, such as `Schema.Struct({ message: Schema.String })`, never captures one.
|
|
62
|
+
- HTTP refuses undeclared input fields, nested ones too, as the OpenAPI document's `additionalProperties: false` says: the request is a 400 `InvalidInput` naming the field's path. A client drops them when it encodes, since TypeScript lets a wider value through. A record accepts every key its key schema does. A CLI command refuses them too, with an `InvalidInput`, in `--input` and in a flag's JSON. A Toolkit drops them, as Effect decodes a model's tool call. An action without input, `input: {}` given or not, accepts only `{}`, on every surface; over HTTP, an empty JSON body is a 400. MCP tools are strict (`Tool.Strict`): undeclared arguments are invalid arguments, an `isError` result from 2025-11-25 on and a JSON-RPC `InvalidParams` error on earlier stdio revisions. An input that declares its fields publishes `additionalProperties: false` in its schema over HTTP and on MCP ([ActionMcp.md](ActionMcp.md#rules)).
|
|
63
|
+
- An `InvalidInput` for input that does not decode lists `issues`, one `{ path, message }` per issue its `message` describes: `path` holds the keys and indexes from the input's root, `["lines", 1, "count"]`, a symbol key as its string form, `Symbol(name)`, and is empty for the input as a whole. Over HTTP a wrong or missing top-level literal field is such an issue, reported as the whole expected shape. A handler failing with `InvalidInput` may give `issues` of its own, sent as written, and `issues: []` without them: the field is always sent, and the OpenAPI document requires it. MCP's and a Toolkit's native answers have a message only.
|
|
64
|
+
- A message for input that does not decode says what the schema expects and, where Effect's decoder reports one, each issue's path, as does each of an `InvalidInput`'s `issues`; never a value sent, on every surface: `InvalidInput` over HTTP, from a CLI command and from `Action.client`, MCP's invalid-arguments result, a Toolkit's `ToolParameterValidationError`. A path names each key it passes through, an undeclared key and a record's keys included, so a record keyed by an email or a token names it. Effect CLI's own parser, which reads a flag before the schema decodes it, names a rejected choice, an unknown flag or an unexpected argument as typed. A schema's own `message` annotation, or a filter's message, is sent as written, so never put the input in one. Declare a secret `Schema.Redacted`, so what a handler logs of its decoded input prints `<redacted>`; a tool call's span holds it all the same ([observability](#observability)).
|
|
65
|
+
- A CLI command prints the success's encoding on stdout, and a failure on stderr as the JSON HTTP sends: the action's declared error, a binding's, a built-in one, `InvalidInput` included. A success that does not encode is a defect there too, which the command does not print ([ActionCli.md](ActionCli.md#rules)).
|
|
66
|
+
- An `Action.client` call passes its input, its success and its failure through their JSON codecs, encoded then decoded, as HTTP carries them, so its handler and its caller get what a remote call's would. A success or a failure that does not pass, an error its action does not declare included, is a defect, as it is an empty 500 over HTTP ([Action.md](Action.md#clients)).
|
|
67
|
+
- A failure an action does not declare, which the types refuse but plain JavaScript or a cast lets through, is answered by each surface its own way: `Action.client` dies with a defect, HTTP answers an empty 500, MCP its internal-error tool result, `Tool execution failed due to an internal server error.`, a local CLI command fails with `UserError` whose `cause` is that failure, and a Toolkit call fails with an `AiError`, `Failed to encode result`. Declare every error a handler may fail with.
|
|
41
68
|
|
|
42
|
-
##
|
|
69
|
+
## Names
|
|
43
70
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
71
|
+
- Action names are unique within one HTTP binding; two bindings with different prefixes may reuse one, but then cannot be combined into one `HttpApi`. A tool is named after its action, so action names are also unique within each Toolkit or MCP endpoint that serves them, and kebab-case command names within each aggregate CLI command. A client's method is too, so they are unique within each `Action.client`.
|
|
72
|
+
- Surfaces validate only the names they serve. HTTP does not check tool names; MCP does not check route names.
|
|
73
|
+
- `ActionHttp.layer`, MCP, a Toolkit and `ActionCli.make` expose the actions their `actions` option lists, or every action of the implementations without it, `ActionHttp.layer` only those its binding holds; `ActionCli.command` exposes only its selected action. `Action.client` exposes every action of the implementations passed to it. A selected action keeps its implementation's authorizer, and its builder's one run. Selection never changes policy: an action's `caller` and authorizer are the same wherever it is served. Builder acquisition follows the [dependency lifetimes](#dependency-lifetimes), including explicit `Action.layer` prebuilding and per-invocation CLI selection: a surface's `actions` leaving out every action of an implementation leaves that implementation unbuilt.
|
|
74
|
+
- Surfaces match an implementation to a contract by object identity, never by name. `ActionHttp.layer` refuses implementations holding none of its binding's actions, and serves no action its binding does not hold, whatever its name; an `actions` option refuses a listed action the binding does not hold, or none of the implementations holds, or `ActionCli.remote` one its binding does not, marking one of a held name as another contract; `ActionCli.command` refuses an action with no implementation in its list.
|
|
75
|
+
- An `actions` option narrows a surface's types only where it is always given. Options whose `actions` may be absent, an optional property or a union with options lacking it, serve every action when it is absent and the listed ones when given, so their types owe what every action owes; `Action.client` offers methods, and `ActionToolkit.make` tools, only for the actions such options may list, which it has either way. An explicit type argument names the options' type, not the actions, and requires the options argument: `ActionMcp.layerHttp<typeof app, { readonly actions: readonly [typeof Status] }>(...)`. A misspelled option is a type error on every surface, beside `actions` too, and in any member of a union of options. A CLI's `commands` key naming no action of the binding or the implementations is refused; one naming an action the list leaves out is unused, so one record serves aggregates of several selections.
|
|
76
|
+
- A list of actions is typed by its members, on every surface and in `Action.byName`: `[A, B]` holds `A` and `B`. A list narrowed at runtime, such as an `Array` of a union, a filtered list or a conditional element, is still typed as holding every member, and what it does not hold is absent where it is read: `byName` has no entry for it, a client no method, and `implement` refuses its handler, `Unknown handlers: <name>`. Build a list from the contracts themselves.
|
|
47
77
|
|
|
48
78
|
## Observability
|
|
49
79
|
|
|
50
|
-
- Each handler runs in a span named
|
|
80
|
+
- Each handler runs in a span named after its action, a child of the transport's request span, or of its caller's through `Action.client`, with attributes `action.name` and `action.read_only`, a boolean. Every log line the handler writes carries the same two annotations. Names are unique per binding; the request span's route tells two bindings' same-named actions apart. Authentication, decoding, the authorizer and encoding happen outside the handler span.
|
|
51
81
|
- MCP defects and encoding failures are logged with their cause and answered generically.
|
|
82
|
+
- Over `ActionHttp` and `ActionMcp.layerHttp`, a handler's defect reaches the `ErrorReporter`s provided to the surface's layer once.
|
|
83
|
+
- Every MCP tool call, over HTTP and over stdio, and every Toolkit call runs through Effect's `Toolkit.handle`, which records the call's arguments as sent on the current span, as the attribute `parameters`: a tracer the host installs receives them, a `Schema.Redacted` field's value included, which a handler's logs print `<redacted>`. HTTP records no request body. Where a tracer exports spans, keep an action that takes a secret off tools ([Action.md](Action.md#contracts)).
|
|
52
84
|
|
|
53
85
|
## MCP transport
|
|
54
86
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- Cancellation is Effect's native RPC interruption. Without an HTTP session, `notifications/cancelled` interrupts nothing over HTTP; a call ends with its request.
|
|
58
|
-
- Each endpoint or subprocess owns a fresh native tool registry. That isolates names, not application context.
|
|
87
|
+
Protocol revisions, transport behavior, cancellation, and tool registry isolation are defined
|
|
88
|
+
in [ActionMcp.md](ActionMcp.md#rules).
|
|
59
89
|
|
|
60
90
|
## Scope
|
|
61
91
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
92
|
+
What the package does, and nothing else:
|
|
93
|
+
|
|
94
|
+
- HTTP means JSON `POST` endpoints built on Effect's `HttpApi`. It is not Effect RPC.
|
|
95
|
+
- MCP means one native `McpServer` `Tool` per action over HTTP or stdio; supported revisions follow [ActionMcp.md](ActionMcp.md#rules). There is no MCP SDK runtime dependency.
|
|
96
|
+
- Actions are unary: one decoded input, one decoded success or one declared error. No streaming, uploads, prompts, resources, retries, or code-execution sandbox.
|
|
97
|
+
- Authentication and authorization are the application's. The library supplies the seams:
|
|
98
|
+
- `caller` on each contract, enforced by every surface;
|
|
99
|
+
- `Authentication.make` and `Authentication.layer`, a named declaration of one native scheme, Bearer by default, and its verifier, enforced and documented with native endpoint security;
|
|
100
|
+
- one `authorize` for the authorization rule, bound to an implementation of protected actions and run by every surface, `Action.allowAll` where there is none;
|
|
101
|
+
- `readOnly`, for what a rule reads;
|
|
102
|
+
- the built-in refusals every caller decodes.
|
|
103
|
+
- It defines no scopes and no verifier: a refusal names the scopes a call lacks, and the library answers with the OAuth challenge. A descriptor names one native scheme, Bearer by default: a session cookie, an API key or Basic is the descriptor's scheme, which its verifier receives decoded. OAuth, its challenges, discovery and step-up, is Bearer's. One identity per action, and one per remote surface. Tool discovery is not filtered by actor. Production needs real token validation, request limits, and error reporting.
|
package/docs/setup.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installation, version pins, entry points, a minimal program, and package boundaries.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pnpm add @gjermundgaraba/effect-actions effect
|
|
8
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.0
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
| Need
|
|
12
|
-
|
|
|
13
|
-
| Node HTTP server
|
|
14
|
-
| `TestingClient.withMcpClient` | `@modelcontextprotocol/client@^2.0.0` (optional peer) |
|
|
11
|
+
| Need | Add |
|
|
12
|
+
| ----------------------------------- | ------------------------------ |
|
|
13
|
+
| Node HTTP server, stdio host or CLI | `@effect/platform-node@~4.0.0` |
|
|
15
14
|
|
|
16
|
-
Node `^22.12.0 || ^24.0.0 || >=26.0.0`.
|
|
15
|
+
Node `^22.12.0 || ^24.0.0 || >=26.0.0`. TypeScript 7 or newer; earlier versions are not
|
|
16
|
+
supported. ESM only.
|
|
17
17
|
|
|
18
18
|
## Entry points
|
|
19
19
|
|
|
@@ -21,45 +21,108 @@ There is no package root. Import one module per subpath, as a namespace:
|
|
|
21
21
|
|
|
22
22
|
```ts
|
|
23
23
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
24
|
-
import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
|
|
25
24
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
26
|
-
import * as ActionHttpClient from "@gjermundgaraba/effect-actions/ActionHttpClient";
|
|
27
25
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
28
26
|
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
29
|
-
import * as ActionCatalog from "@gjermundgaraba/effect-actions/ActionCatalog";
|
|
30
27
|
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
31
|
-
import * as ActionCliClient from "@gjermundgaraba/effect-actions/ActionCliClient";
|
|
32
28
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
33
29
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
34
|
-
import * as TestingClient from "@gjermundgaraba/effect-actions/TestingClient";
|
|
35
30
|
```
|
|
36
31
|
|
|
37
|
-
Every module imports from the `effect` package only
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
Every module imports from the `effect` package only. There is no package root, so importing
|
|
33
|
+
`Action` or `ActionHttp` never pulls in an MCP server or the AI toolkit.
|
|
34
|
+
|
|
35
|
+
## Minimal program
|
|
36
|
+
|
|
37
|
+
```ts example=quickstart.ts
|
|
38
|
+
import { Schema } from "effect";
|
|
39
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
40
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
41
|
+
|
|
42
|
+
// The contract and its HTTP binding import no server code, so any client can import them,
|
|
43
|
+
// a browser page included.
|
|
44
|
+
export const Greet = Action.make("greet", {
|
|
45
|
+
description: "Greet someone by name.",
|
|
46
|
+
input: { name: Schema.String },
|
|
47
|
+
success: Schema.String,
|
|
48
|
+
readOnly: true,
|
|
49
|
+
caller: Action.Anyone,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const Http = ActionHttp.make([Greet]);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The server, in its own module, so that a browser client imports the contract alone:
|
|
56
|
+
|
|
57
|
+
```ts example=quickstart-server.ts
|
|
58
|
+
import { Effect, Layer } from "effect";
|
|
59
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
60
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
61
|
+
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
62
|
+
import { Greet, Http } from "./quickstart.js";
|
|
63
|
+
|
|
64
|
+
// The contract states who may call it, here anyone, so its implementation takes no `authorize`.
|
|
65
|
+
const greet = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
|
|
66
|
+
|
|
67
|
+
export const routes = Layer.mergeAll(
|
|
68
|
+
ActionHttp.layer(Http, greet),
|
|
69
|
+
ActionMcp.layerHttp(greet, { name: "greetings", version: "1.0.0" }),
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Serve `routes` with `HttpRouter.serve` and a platform server layer, with a request body limit ([guarantees.md](guarantees.md#wire-behavior)). Result: `POST /api/greet` and an MCP tool `greet` at `/mcp`. Anything else the process runs, such as a job or an agent, goes inside the layer `HttpRouter.serve` serves, not merged beside it: beside it, the builders and your own services it shares with the routes may run twice, each with state of its own, and nothing reports it ([dependency lifetimes](guarantees.md#dependency-lifetimes)).
|
|
74
|
+
|
|
75
|
+
## Browser
|
|
76
|
+
|
|
77
|
+
A browser app calls the server with `ActionHttp.client`, or `ActionHttp.fetchClient` outside an
|
|
78
|
+
Effect. It needs the contracts, their identities, the HTTP binding and its authentication
|
|
79
|
+
descriptor, and nothing else of the server. The package
|
|
80
|
+
declares `"sideEffects": false`, so a bundler may drop what a client does not use. Keep the
|
|
81
|
+
contracts and the binding in modules that import no server code, and import those alone from
|
|
82
|
+
the page, as the minimal program does:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
identity.ts Context.Service declarations a protected contract names: imports effect only
|
|
86
|
+
contracts.ts Action.make(...), Authentication.make(...) and ActionHttp.make([...])
|
|
87
|
+
handlers.ts Action.implement(...): services, database, @effect/platform-node
|
|
88
|
+
server.ts ActionHttp.layer, ActionMcp, Authentication.layer and its verifier
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A module that calls `Action.implement` beside its contracts brings its handlers, and every
|
|
92
|
+
module they import, into the browser bundle: bundlers keep the call as written, even unused.
|
|
93
|
+
Split it. A page on another origin also needs CORS on the host, outside authentication: see
|
|
94
|
+
the browser example in [ActionMcp.md](ActionMcp.md#cross-origin-browsers).
|
|
95
|
+
|
|
96
|
+
`Action`, `ActionHttp` and `Authentication`, and every module they import, import Effect through three
|
|
97
|
+
specifiers alone: `effect`, `effect/http` and `effect/http-api`. A page that loads Effect from
|
|
98
|
+
an import map rather than its bundle maps those three. A map pointing at Effect 4.0.0's
|
|
99
|
+
published files maps `effect/Cause`, `effect/Effect`, `effect/Exit` and `effect/Function` too,
|
|
100
|
+
to the same files the `effect` barrel loads, as Effect's own `Runtime` module imports them by
|
|
101
|
+
name; a build or CDN that resolves Effect's own imports needs only the three.
|
|
102
|
+
Serve each of the three whole. A page that re-exports only the names it uses, from a vendored
|
|
103
|
+
module or a trimmed bundle, fails at load with `does not provide an export named …` once the
|
|
104
|
+
package imports another name from the same specifier, as a release may: check such a list
|
|
105
|
+
against the package's imports on every upgrade.
|
|
42
106
|
|
|
43
107
|
Companion Effect modules you will import alongside: `effect/http-api` (`HttpApiClient`, `OpenApi`, `HttpApiSwagger`, `HttpApiScalar`), `effect/http` (`HttpRouter`, `HttpServerResponse`, `FetchHttpClient`, `HttpClientError`), `effect/cli` (`Command`, `Flag`, `Argument`).
|
|
44
108
|
|
|
45
109
|
## Rules
|
|
46
110
|
|
|
47
|
-
- Install a single `effect` version within the peer range, and keep every `@effect/*` package on that same release
|
|
111
|
+
- Install a single `effect` version within the peer range, and keep every `@effect/*` package on that same release. Mixed versions can fail at the type level in ways that look like library bugs.
|
|
48
112
|
- Never import from `dist/` paths or from a package root. Only the subpaths above are public.
|
|
49
113
|
- Schemas passed to `Action.make` must be service-free (`Schema.Codec<_, _, never, never>`). Handlers may require services.
|
|
50
114
|
|
|
51
115
|
## Scope
|
|
52
116
|
|
|
53
|
-
What the package does
|
|
54
|
-
|
|
55
|
-
- HTTP means JSON `POST` endpoints built on Effect's `HttpApi`. It is not Effect RPC.
|
|
56
|
-
- MCP means one native `McpServer` `Tool` per action, over MCP 2026-07-28 only. There is no MCP SDK runtime dependency.
|
|
57
|
-
- Actions are unary: one decoded input, one decoded success or one declared error. No streaming, uploads, prompts, resources, retries, or code-execution sandbox.
|
|
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.
|
|
117
|
+
What the package does and does not do: [guarantees.md](guarantees.md#scope).
|
|
59
118
|
|
|
60
119
|
## Failure modes
|
|
61
120
|
|
|
62
121
|
- `Cannot find module '@gjermundgaraba/effect-actions'`: there is no root export. Import a subpath.
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
122
|
+
- A browser build fails with `Could not resolve "node:…"`, or warns `Module "node:…" has been externalized for browser compatibility`: the page imports a module that also holds server code, such as a contract beside its `Action.implement`, or an authentication descriptor beside its `Authentication.layer` verifier. Move the contracts, their identities, the descriptor and the binding to modules that import no server code.
|
|
123
|
+
- A page that loads Effect from an import map still bundles a second copy of Effect, or of some of its modules: a module of the page imports an Effect specifier the map does not serve, such as `effect/Schema`. Serve `effect`, `effect/http` and `effect/http-api`, all `Action`, `ActionHttp` and `Authentication` import, and import Effect through them in the page's own modules too.
|
|
124
|
+
- A page loading Effect from an import map fails before its modules run, with `Failed to resolve module specifier "effect/Cause"` in Chromium: the map points at Effect's published files and lacks a specifier Effect imports itself. Map `effect/Cause`, `effect/Effect`, `effect/Exit` and `effect/Function` to the same files, or serve Effect from a build that resolves its own imports.
|
|
125
|
+
- Type errors inside `effect/*` modules after install: `effect` version drift. Install one `effect` 4.0.x release for every package.
|
|
126
|
+
- An unmet peer warning for `effect@~4.0.0`, such as `found 4.1.0`: the installed Effect is a later minor than this release has been tested against. Install `effect` 4.0.x, with every `@effect/*` package on the same release, until a release of this package admits that minor.
|
|
127
|
+
- `Cannot find module 'effect/unstable/http'` (or `…/httpapi`, `…/cli`, `…/ai`): Effect `4.0.0-rc.118` moved these modules to `effect/http`, `effect/http-api`, `effect/cli` and `effect/ai`. Import the new paths.
|
|
128
|
+
- `Cannot find module 'effect/http'` (or `effect/http-api`, `effect/cli`, `effect/ai`), or at run time `Cannot find module '…/node_modules/effect/dist/http-api.js'` (or `http.js`, `ai.js`, `cli.js`): `effect` is a release candidate older than `4.0.0-rc.118`, which moved these modules out of `effect/unstable/*`. Install `effect` 4.0.0 or later.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Define Effect action contracts once and project them onto HTTP, MCP, native Toolkits,
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Define Effect action contracts once and project them onto HTTP, MCP, native Toolkits, and CLIs.",
|
|
5
5
|
"homepage": "https://github.com/gjermundgaraba/effect-actions#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/gjermundgaraba/effect-actions/issues"
|
|
@@ -16,42 +16,34 @@
|
|
|
16
16
|
"docs"
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
19
20
|
"exports": {
|
|
20
21
|
"./Action": "./dist/Action.mjs",
|
|
21
|
-
"./ActionCatalog": "./dist/ActionCatalog.mjs",
|
|
22
22
|
"./ActionCli": "./dist/ActionCli.mjs",
|
|
23
|
-
"./ActionCliClient": "./dist/ActionCliClient.mjs",
|
|
24
|
-
"./ActionGroup": "./dist/ActionGroup.mjs",
|
|
25
23
|
"./ActionHttp": "./dist/ActionHttp.mjs",
|
|
26
|
-
"./ActionHttpClient": "./dist/ActionHttpClient.mjs",
|
|
27
24
|
"./ActionMcp": "./dist/ActionMcp.mjs",
|
|
28
25
|
"./ActionToolkit": "./dist/ActionToolkit.mjs",
|
|
29
26
|
"./Authentication": "./dist/Authentication.mjs",
|
|
30
27
|
"./Testing": "./dist/Testing.mjs",
|
|
31
|
-
"./TestingClient": "./dist/TestingClient.mjs",
|
|
32
28
|
"./package.json": "./package.json"
|
|
33
29
|
},
|
|
34
30
|
"publishConfig": {
|
|
35
31
|
"access": "public"
|
|
36
32
|
},
|
|
37
33
|
"devDependencies": {
|
|
38
|
-
"@effect/platform-node": "4.0.0
|
|
34
|
+
"@effect/platform-node": "4.0.0",
|
|
35
|
+
"@effect/vitest": "4.0.0",
|
|
39
36
|
"@modelcontextprotocol/client": "2.0.0",
|
|
40
37
|
"@types/node": "26.6.2",
|
|
41
|
-
"effect": "4.0.0
|
|
38
|
+
"effect": "4.0.0",
|
|
42
39
|
"tsx": "4.23.15",
|
|
43
40
|
"typescript": "7.0.2",
|
|
44
41
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
|
|
45
|
-
"vite-plus": "1.0.0"
|
|
42
|
+
"vite-plus": "1.0.0",
|
|
43
|
+
"vitest": "5.0.1"
|
|
46
44
|
},
|
|
47
45
|
"peerDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"effect": ">=4.0.0-rc.118 <4.0.0"
|
|
50
|
-
},
|
|
51
|
-
"peerDependenciesMeta": {
|
|
52
|
-
"@modelcontextprotocol/client": {
|
|
53
|
-
"optional": true
|
|
54
|
-
}
|
|
46
|
+
"effect": "~4.0.0"
|
|
55
47
|
},
|
|
56
48
|
"devEngines": {
|
|
57
49
|
"packageManager": {
|
package/dist/ActionCatalog.d.mts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Action_d_exports, Any } from "./Action.mjs";
|
|
2
|
-
import { ActionGroup_d_exports, Any as Any$1 } from "./ActionGroup.mjs";
|
|
3
|
-
import { JsonSchema } from "effect";
|
|
4
|
-
//#region src/ActionCatalog.d.ts
|
|
5
|
-
/** One contract, with independently rooted descriptions of its encoded values. */
|
|
6
|
-
export interface Entry {
|
|
7
|
-
readonly id: string;
|
|
8
|
-
readonly group: string;
|
|
9
|
-
readonly name: string;
|
|
10
|
-
readonly description: string;
|
|
11
|
-
readonly mcp: Any["mcp"];
|
|
12
|
-
readonly input: JsonSchema.JsonSchema;
|
|
13
|
-
readonly success: JsonSchema.JsonSchema;
|
|
14
|
-
/** Includes the errors inherited from the group; not HTTP schema-policy errors. */
|
|
15
|
-
readonly errors: ReadonlyArray<JsonSchema.JsonSchema>;
|
|
16
|
-
/** HTTP-only decoding/encoding policy errors, separate from handler failures. */
|
|
17
|
-
readonly httpSchemaErrors: ReadonlyArray<JsonSchema.JsonSchema>;
|
|
18
|
-
}
|
|
19
|
-
/** An offline contract document, not an authorization decision or a mounted endpoint. */
|
|
20
|
-
export interface Catalog {
|
|
21
|
-
readonly version: "4";
|
|
22
|
-
readonly actions: ReadonlyArray<Entry>;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Describe the supplied groups without acquiring implementations. Schemas describe
|
|
26
|
-
* encoded action values, not MCP's `{ value }` envelope or deployment-specific URLs.
|
|
27
|
-
* The host chooses what, if anything, to publish.
|
|
28
|
-
*/
|
|
29
|
-
export declare const make: (...groups: ReadonlyArray<Any$1>) => Catalog;
|
|
30
|
-
//#endregion
|
package/dist/ActionCatalog.mjs
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { assertDistinct, policyErrors } from "./internal/actions.mjs";
|
|
2
|
-
import { JsonSchema, Schema } from "effect";
|
|
3
|
-
//#region src/ActionCatalog.ts
|
|
4
|
-
/**
|
|
5
|
-
* Describe the JSON wire form closed, as the OpenAPI document does; decoding on the
|
|
6
|
-
* server remains authoritative.
|
|
7
|
-
*/
|
|
8
|
-
const describe = (codec) => {
|
|
9
|
-
const { schema, definitions } = Schema.toJsonSchemaDocument(codec, { onExcessProperty: "error" });
|
|
10
|
-
return {
|
|
11
|
-
$schema: JsonSchema.META_SCHEMA_URI_DRAFT_2020_12,
|
|
12
|
-
...schema,
|
|
13
|
-
...Object.keys(definitions).length === 0 ? {} : { $defs: definitions }
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Describe the supplied groups without acquiring implementations. Schemas describe
|
|
18
|
-
* encoded action values, not MCP's `{ value }` envelope or deployment-specific URLs.
|
|
19
|
-
* The host chooses what, if anything, to publish.
|
|
20
|
-
*/
|
|
21
|
-
const make = (...groups) => {
|
|
22
|
-
assertDistinct("catalog group", groups.map((group) => group.name));
|
|
23
|
-
return {
|
|
24
|
-
version: "4",
|
|
25
|
-
actions: groups.flatMap((group) => group.actions.map((action) => ({
|
|
26
|
-
id: `${group.name}.${action.name}`,
|
|
27
|
-
group: group.name,
|
|
28
|
-
name: action.name,
|
|
29
|
-
description: action.description,
|
|
30
|
-
mcp: action.mcp,
|
|
31
|
-
input: describe(action.input),
|
|
32
|
-
success: describe(action.success),
|
|
33
|
-
errors: action.errors.map(describe),
|
|
34
|
-
httpSchemaErrors: group.schemaError === void 0 ? [] : policyErrors(group.schemaError).map(describe)
|
|
35
|
-
})))
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
//#endregion
|
|
39
|
-
export { make };
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Action_d_exports, Codec } from "./Action.mjs";
|
|
2
|
-
import { Actions } from "./internal/actions.mjs";
|
|
3
|
-
import { Options as Options$1 } from "./internal/cli.mjs";
|
|
4
|
-
import { ActionHttp_d_exports, Api, Http } from "./ActionHttp.mjs";
|
|
5
|
-
import { Effect } from "effect";
|
|
6
|
-
import { HttpClient } from "effect/http";
|
|
7
|
-
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
|
|
8
|
-
import { Command } from "effect/cli";
|
|
9
|
-
//#region src/ActionCliClient.d.ts
|
|
10
|
-
/**
|
|
11
|
-
* What the host configures on Effect's native client: `baseUrl`, `transformClient`,
|
|
12
|
-
* `transformResponse`. The selectors and the client are the command's own.
|
|
13
|
-
*/
|
|
14
|
-
export type Connection = NonNullable<globalThis.Parameters<typeof HttpApiClient.make>[1]>;
|
|
15
|
-
/** Parsing, rendering and native HTTP-endpoint configuration for one remote action. */
|
|
16
|
-
export type Options<Output, ParsedParameters extends Command.Command.Config = never> = Options$1<Output, ParsedParameters> & {
|
|
17
|
-
readonly connection?: Connection;
|
|
18
|
-
};
|
|
19
|
-
/** Configuration for a remote aggregate group command. */
|
|
20
|
-
export interface GroupOptions {
|
|
21
|
-
/** Override the group command name. */
|
|
22
|
-
readonly name?: string;
|
|
23
|
-
/** Shared native HTTP-endpoint configuration for every remote action. */
|
|
24
|
-
readonly connection?: Connection;
|
|
25
|
-
}
|
|
26
|
-
type Group<Groups extends ReadonlyArray<Actions>, Name extends Groups[number]["name"]> = Extract<Groups[number], {
|
|
27
|
-
readonly name: Name;
|
|
28
|
-
}>;
|
|
29
|
-
type Selected<G extends Actions, Name extends G["actions"][number]["name"]> = Extract<G["actions"][number], {
|
|
30
|
-
readonly name: Name;
|
|
31
|
-
}>;
|
|
32
|
-
type ActionNames<G extends Actions> = G["actions"][number]["name"];
|
|
33
|
-
type ApiGroups<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>> = Api<Groups[number], Errors[number]> extends HttpApi.HttpApi<"actions", infer ApiGroups> ? ApiGroups : never;
|
|
34
|
-
type Endpoint<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends ActionNames<Group<Groups, GroupName>>> = Extract<HttpApiGroup.EndpointsWithIdentifier<ApiGroups<Groups, Errors>, Extract<GroupName, HttpApiGroup.Identifier<ApiGroups<Groups, Errors>>>>, {
|
|
35
|
-
readonly identifier: ActionName;
|
|
36
|
-
}>;
|
|
37
|
-
type NativeMethod<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends ActionNames<Group<Groups, GroupName>>> = HttpApiClient.Client.Method<Extract<Endpoint<Groups, Errors, GroupName, ActionName>, HttpApiEndpoint.ConstraintRequest>, never, never>;
|
|
38
|
-
type RemoteCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"], ActionName extends ActionNames<Group<Groups, GroupName>>> = Command.Command<string, never, {}, Effect.Error<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionName>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionName>>>>;
|
|
39
|
-
type RemoteGroupCommand<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec>, GroupName extends Groups[number]["name"]> = Command.Command<string, {}, {}, Effect.Error<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionNames<Group<Groups, GroupName>>>>>, HttpClient.HttpClient | Effect.Services<ReturnType<NativeMethod<Groups, Errors, GroupName, ActionNames<Group<Groups, GroupName>>>>>>;
|
|
40
|
-
/**
|
|
41
|
-
* Project one action of the binding into a native Effect CLI command.
|
|
42
|
-
* Group and action selectors resolve exclusively from `http.groups`; runtime guards
|
|
43
|
-
* keep dynamically supplied selector strings from reaching the native client.
|
|
44
|
-
*/
|
|
45
|
-
export declare const command: <const Groups extends ReadonlyArray<Actions>, const Errors extends ReadonlyArray<Codec>, const GroupName extends Groups[number]["name"], const ActionName extends ActionNames<Group<Groups, GroupName>>, ParsedParameters extends Command.Command.Config = never>(http: Http<Groups, Errors>, groupName: GroupName, actionName: ActionName, options?: Options<Selected<Group<Groups, GroupName>, ActionName>["success"]["Type"], ParsedParameters>) => RemoteCommand<Groups, Errors, GroupName, ActionName>;
|
|
46
|
-
/** Project every action of one bound group below its group namespace. */
|
|
47
|
-
export declare const group: <const Groups extends ReadonlyArray<Actions>, const Errors extends ReadonlyArray<Codec>, const GroupName extends Groups[number]["name"]>(http: Http<Groups, Errors>, groupName: GroupName, options?: GroupOptions) => RemoteGroupCommand<Groups, Errors, GroupName>;
|
|
48
|
-
//#endregion
|