@gjermundgaraba/effect-actions 0.10.0 → 0.12.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 +103 -43
- package/dist/Action.d.mts +31 -118
- package/dist/Action.mjs +15 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +25 -31
- package/dist/ActionMcp.mjs +140 -74
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +5 -8
- package/dist/ActionToolkit.mjs +3 -3
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +32 -25
- package/dist/Testing.mjs +45 -30
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +17 -0
- package/dist/mcp/protocol.mjs +149 -0
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +206 -424
- package/docs/ActionCli-failures.md +102 -0
- package/docs/ActionCli.md +206 -292
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +202 -281
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +229 -193
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +178 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +100 -117
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +142 -392
- package/docs/CONTEXT.md +39 -23
- package/docs/README.md +71 -35
- package/docs/Testing-failures.md +20 -0
- package/docs/Testing.md +186 -151
- package/docs/guarantees.md +35 -95
- package/docs/setup-failures.md +34 -0
- package/docs/setup.md +88 -87
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +317 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +75 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +136 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +119 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +30 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +108 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +89 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +91 -0
- package/examples/notes-service/src/notes/handlers.ts +33 -0
- package/examples/notes-service/src/notes/store.ts +73 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +134 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +32 -14
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -11
- package/dist/internal/mcp.mjs +0 -52
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Protected, SelectedOf, Selection, Served, ServedRequest } from "./
|
|
1
|
+
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Protected, SelectedOf, Selection, Served, ServedRequest } from "./contract/implementation.mjs";
|
|
2
2
|
import { Action_d_exports, Any } from "./Action.mjs";
|
|
3
|
-
import { Any as Any$1, Matching, ProviderOf, RemoteRequest, Required } from "./
|
|
3
|
+
import { Any as Any$1, Matching, ProviderOf, RemoteRequest, Required, ServerBuildServices } from "./authentication/provider.mjs";
|
|
4
4
|
import { Cause, Effect, Layer, Stdio } from "effect";
|
|
5
|
-
import { HttpRouter } from "effect/http";
|
|
6
5
|
import { McpSchema, McpServer } from "effect/ai";
|
|
7
|
-
|
|
6
|
+
import { HttpRouter } from "effect/http";
|
|
7
|
+
//#region src/mcp/ActionMcp.d.ts
|
|
8
8
|
/**
|
|
9
9
|
* An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option except
|
|
10
10
|
* `protocols`, the server information, `instructions` and `extensions`, the actions `A` that
|
|
@@ -24,58 +24,52 @@ export interface Options<E = never, R = never, A extends Any = Any> extends Omit
|
|
|
24
24
|
readonly features?: Layer.Layer<never, E, R> | undefined;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* One Streamable HTTP MCP endpoint
|
|
28
|
-
*
|
|
27
|
+
* One Streamable HTTP MCP endpoint, speaking MCP 2026-07-28, 2025-11-25 or 2025-06-18, as the
|
|
28
|
+
* client negotiates: `Options`, and every native `McpServer.layerHttp` option, `allowedOrigins`
|
|
29
|
+
* among them, except `protocols` and `allowSessionTermination`, which are fixed, with `path`
|
|
30
|
+
* defaulting to `/mcp`. A client of a 2025 revision opens a session with `initialize`, which
|
|
31
|
+
* lasts until that client ends it with a DELETE, answered 204, or the process stops.
|
|
29
32
|
*/
|
|
30
|
-
export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> extends Options<E, R, A>, Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols" | "path"> {
|
|
33
|
+
export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> extends Options<E, R, A>, Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols" | "path" | "allowSessionTermination"> {
|
|
31
34
|
/** The endpoint's route; defaults to `/mcp`. */
|
|
32
35
|
readonly path?: HttpRouter.PathInput;
|
|
33
36
|
readonly authentication?: Any$1;
|
|
34
37
|
}
|
|
35
|
-
/**
|
|
36
|
-
* The native server supplies its own request context to every tool call, and the registry
|
|
37
|
-
* it registers the tools on, the endpoint's own; over HTTP the router its own, such as the
|
|
38
|
-
* request.
|
|
39
|
-
*/
|
|
40
38
|
type ToolRequestContext<R> = Exclude<R, McpSchema.McpRequestContext | McpServer.McpServer>;
|
|
41
39
|
type HttpToolRequestContext<R> = Exclude<ToolRequestContext<R>, HttpRouter.Provided>;
|
|
42
|
-
/**
|
|
43
|
-
* What the native features owe, `R`, beside the registry, which the endpoint provides them,
|
|
44
|
-
* so one registering through `McpServer.registerResource` owes it no host.
|
|
45
|
-
*/
|
|
46
40
|
type Features<R> = Exclude<R, McpServer.McpServer>;
|
|
47
|
-
/**
|
|
48
|
-
* Where `D`, the descriptor given, is inferred, so the provider owed is that descriptor's.
|
|
49
|
-
* Without one, tools `A` all public take none, even past a `D` given explicitly; protected
|
|
50
|
-
* ones state theirs in `RequiredAuthentication` alone, as an absent `authentication` and the
|
|
51
|
-
* one they require would intersect to `never`, reading as every option being wrong.
|
|
52
|
-
*/
|
|
53
41
|
type Inferring<D, A extends Any> = [D] extends [undefined] ? [Protected<A>] extends [never] ? {
|
|
54
42
|
readonly authentication?: undefined;
|
|
55
43
|
} : unknown : {
|
|
56
44
|
readonly authentication?: D;
|
|
57
45
|
};
|
|
58
46
|
/**
|
|
59
|
-
* Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28
|
|
47
|
+
* Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28, 2025-11-25 or
|
|
48
|
+
* 2025-06-18, as the client negotiates. A 2026-07-28 request stands alone; a client of a 2025
|
|
49
|
+
* revision opens a session with `initialize`, which holds what it sent there and no identity,
|
|
50
|
+
* and lasts until that client ends it with a DELETE, answered 204, or the process stops.
|
|
60
51
|
*
|
|
61
52
|
* Protected tools need the `authentication` descriptor their contracts name, and the
|
|
62
53
|
* provider `Authentication.layer` builds from it. On an endpoint of protected tools only,
|
|
63
54
|
* every request authenticates before it is decoded. On a mixed endpoint, anyone may discover
|
|
64
|
-
* the server, list its tools, cancel, and call a public tool
|
|
65
|
-
* first,
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
55
|
+
* the server, list its tools, cancel, and call a public tool in a 2026-07-28 request;
|
|
56
|
+
* everything else authenticates first, a session's every request included, so a protected
|
|
57
|
+
* tool's description is listed to anyone but run only for its caller.
|
|
58
|
+
* On a Bearer endpoint, a protected tool's call refused with `Unauthenticated`, or with
|
|
59
|
+
* `Forbidden` naming scopes, is answered with its HTTP status and challenge, 401 or 403, as MCP
|
|
60
|
+
* authorization requires, unless its response has started; any other failure is a tool result. Claude Code connects with
|
|
61
|
+
* `claude mcp add --transport http <name> <url>`. In a tool call, what middleware provides per request wins over
|
|
69
62
|
* what the endpoint was built with; native `features` read only what they were built with,
|
|
70
63
|
* never a request's. Never provide an identity at startup: a tool call takes its identity
|
|
71
64
|
* only from the request it authenticated.
|
|
65
|
+
* Guide: `docs/ActionMcp.md` in this package; in an application: `examples/mcp-tools/`.
|
|
72
66
|
*/
|
|
73
|
-
export declare function layerHttp<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never, const D extends Any$1 | undefined = undefined>(implementations: Apps, options: LayerHttpOptions<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, LayerHttpOptions<E, R, Any>>> & NoInfer<Required<SelectedOf<O, Apps>>> & Matching<NoInfer<SelectedOf<O, Apps>>, NoInfer<D>> & Inferring<D, NoInfer<SelectedOf<O, Apps>>>): Layer.Layer<never, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E,
|
|
67
|
+
export declare function layerHttp<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never, const D extends Any$1 | undefined = undefined>(implementations: Apps, options: LayerHttpOptions<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, LayerHttpOptions<E, R, Any>>> & NoInfer<Required<SelectedOf<O, Apps>>> & Matching<NoInfer<SelectedOf<O, Apps>>, NoInfer<D>> & Inferring<D, NoInfer<SelectedOf<O, Apps>>>): Layer.Layer<never, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, ServerBuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", HttpToolRequestContext<RemoteRequest<Member<Apps>, SelectedOf<O, Apps>>>> | ProviderOf<D> | Features<R>>;
|
|
74
68
|
/**
|
|
75
69
|
* Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
|
|
76
70
|
* 2026-07-28, 2025-11-25 or 2025-06-18, as the host negotiates: the whole
|
|
77
|
-
* program of an MCP subprocess, which succeeds once the host closes its side and
|
|
78
|
-
* flight
|
|
71
|
+
* program of an MCP subprocess, which succeeds once the host closes its side and every call
|
|
72
|
+
* in flight has been answered. A signal interrupts it.
|
|
79
73
|
*
|
|
80
74
|
* Effect logs go to stderr, since stdout carries the protocol. The host supplies the
|
|
81
75
|
* `Stdio` service and the identity. Arguments are tool input only and never establish
|
package/dist/ActionMcp.mjs
CHANGED
|
@@ -1,105 +1,170 @@
|
|
|
1
|
-
import { Anyone } from "./
|
|
2
|
-
import { provideHandlers, select, toList } from "./
|
|
3
|
-
import {
|
|
4
|
-
import { httpProtocol } from "./
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { Cause, Context, Effect, Exit, Fiber, JsonPointer, Layer, Predicate, Schema } from "effect";
|
|
1
|
+
import { Anyone } from "./contract/rules.mjs";
|
|
2
|
+
import { provideHandlers, select, toList } from "./contract/implementation.mjs";
|
|
3
|
+
import { bindTools } from "./toolkit/tools.mjs";
|
|
4
|
+
import { fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, protocols } from "./mcp/protocol.mjs";
|
|
5
|
+
import { assertAuthentication, promote, providerOf } from "./authentication/provider.mjs";
|
|
6
|
+
import { recordStepUp } from "./authentication/refusal.mjs";
|
|
7
|
+
import { withStderrConsole } from "./stdio/console.mjs";
|
|
8
|
+
import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, References, Result, Schema, Stream } from "effect";
|
|
9
|
+
import { AiError, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
|
|
9
10
|
import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The revisions served over stdio: 2026-07-28 and the stateful revisions hosts open stdio
|
|
14
|
-
* with, newest first; a host asking for another is offered 2025-11-25. A success is sent as it
|
|
15
|
-
* is on each. 2026-07-28 carries any success as `structuredContent`; the adapters of 2025-11-25
|
|
16
|
-
* and 2025-06-18 carry only an object there, and list only an object-rooted `outputSchema`. A
|
|
17
|
-
* success they do not structure is text alone, a string as itself.
|
|
18
|
-
*/
|
|
19
|
-
const stdioProtocols = [
|
|
20
|
-
McpProtocol.v2026_07_28,
|
|
21
|
-
McpProtocol.v2025_11_25,
|
|
22
|
-
McpProtocol.v2025_06_18
|
|
23
|
-
];
|
|
24
|
-
/**
|
|
25
|
-
* MCP has a JSON-only wire contract. Success is the encoded success as structured content,
|
|
26
|
-
* and its JSON as text; declared failures are returned as JSON text. The native server
|
|
27
|
-
* refuses undeclared arguments and publishes closed input schemas.
|
|
28
|
-
*/
|
|
29
|
-
const projection = {
|
|
11
|
+
//#region src/mcp/ActionMcp.ts
|
|
12
|
+
const strictToolProjection = {
|
|
30
13
|
label: "MCP tool",
|
|
31
14
|
tool: (tool) => tool.annotate(Tool.Strict, true)
|
|
32
15
|
};
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
* The JSON Schema root of `schema` as the native server lists it for a tool, a `$ref` at its
|
|
37
|
-
* root resolved.
|
|
38
|
-
*/
|
|
39
|
-
const rootOf = (schema) => {
|
|
40
|
-
const document = Schema.toJsonSchemaDocument(Schema.toCodecJson(schema));
|
|
16
|
+
const isToolArgumentsSchema = Schema.is(McpSchema.ToolJson);
|
|
17
|
+
const nativeToolJsonSchema = (schema, strict) => {
|
|
18
|
+
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
|
|
41
19
|
const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
|
|
42
|
-
|
|
20
|
+
const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
|
|
21
|
+
return root === void 0 || Object.keys(document.definitions).length === 0 ? root : {
|
|
22
|
+
...root,
|
|
23
|
+
$defs: document.definitions
|
|
24
|
+
};
|
|
43
25
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* root of a tool's arguments must be, naming every one, reading the JSON Schema the native
|
|
47
|
-
* server reads: not a union, an array, a scalar, nor `Schema.Struct({})`, which takes any value
|
|
48
|
-
* but `null`, where the native server dies when the layer builds, suggesting
|
|
49
|
-
* `Tool.EmptyParams`.
|
|
50
|
-
*/
|
|
51
|
-
const assertShapes = (apps) => {
|
|
52
|
-
const inputs = apps.flatMap((app) => app.actions).filter((action) => !isToolJson(rootOf(action.input))).map(({ name }) => name);
|
|
26
|
+
const assertToolShapes = (lifted) => {
|
|
27
|
+
const inputs = lifted.map(([action]) => action).filter((action) => !isToolArgumentsSchema(nativeToolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
|
|
53
28
|
if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
|
|
29
|
+
const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
|
|
30
|
+
if (misplaced.length > 0) throw new Error(`MCP media must be the success or an array of it, or a top-level field of a struct success, one, optional or a required array: ${misplaced.join(", ")}`);
|
|
54
31
|
};
|
|
32
|
+
const nativeInternalErrorText = "Tool execution failed due to an internal server error.";
|
|
33
|
+
const textErrorResult = (text) => new McpSchema.CallToolResult({
|
|
34
|
+
isError: true,
|
|
35
|
+
content: [{
|
|
36
|
+
type: "text",
|
|
37
|
+
text
|
|
38
|
+
}]
|
|
39
|
+
});
|
|
40
|
+
const nativeJsonTextOf = (encoded) => encoded === void 0 ? [] : [{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: JSON.stringify(encoded)
|
|
43
|
+
}];
|
|
44
|
+
const contentBlockOf = ({ kind, field, value }) => ({
|
|
45
|
+
type: kind,
|
|
46
|
+
data: value.data,
|
|
47
|
+
mimeType: value.mimeType,
|
|
48
|
+
...field === void 0 ? {} : { _meta: { [fieldKey]: field } }
|
|
49
|
+
});
|
|
50
|
+
const parameterValidationErrorOf = (error) => AiError.isAiError(error) && Predicate.isTagged(error.reason, "ToolParameterValidationError") ? error.reason : void 0;
|
|
51
|
+
const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
|
|
52
|
+
const registerToolsLiftingMedia = Effect.fnUntraced(function* (toolkit, lifts) {
|
|
53
|
+
const registry = yield* McpServer.McpServer;
|
|
54
|
+
const built = yield* toolkit.pipe(Effect.updateContext((context) => Context.merge(context, Context.mergeAll(...Object.values(toolkit.tools).flatMap((tool) => {
|
|
55
|
+
const key = Context.Service(tool.id);
|
|
56
|
+
return Option.toArray(Context.getOption(context, key)).map((handler) => Context.make(key, {
|
|
57
|
+
...handler,
|
|
58
|
+
context: omitRequestServices(handler.context)
|
|
59
|
+
}));
|
|
60
|
+
})))));
|
|
61
|
+
const retainedServices = omitRequestServices(yield* Effect.context());
|
|
62
|
+
const propagateInterruptOrReportInternal = (cause) => {
|
|
63
|
+
const failure = Cause.findFail(cause);
|
|
64
|
+
return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(Effect.provideContext(ErrorReporter.report(cause), retainedServices)), Effect.as(textErrorResult(nativeInternalErrorText)));
|
|
65
|
+
};
|
|
66
|
+
const invalidParamsOrInternal = (cause) => {
|
|
67
|
+
const failure = Cause.findFail(cause);
|
|
68
|
+
if (Result.isSuccess(failure)) {
|
|
69
|
+
const { error } = failure.success;
|
|
70
|
+
const invalid = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin) === "parameters" ? parameterValidationErrorOf(error) : void 0;
|
|
71
|
+
if (invalid !== void 0) return Effect.fail(new McpSchema.InvalidParams({ message: invalid.message }));
|
|
72
|
+
}
|
|
73
|
+
return propagateInterruptOrReportInternal(cause);
|
|
74
|
+
};
|
|
75
|
+
const registrations = yield* Effect.forEach(Object.entries(built.tools), ([name, tool]) => Effect.gen(function* () {
|
|
76
|
+
const strict = Tool.getStrictMode(tool) === true;
|
|
77
|
+
const decodeOptions = {
|
|
78
|
+
onExcessProperty: strict ? "error" : "ignore",
|
|
79
|
+
errors: "all"
|
|
80
|
+
};
|
|
81
|
+
const lift = lifts.get(name);
|
|
82
|
+
const media = lift === void 0 || lift.fields.length === 0 ? void 0 : lift;
|
|
83
|
+
const structured = media === void 0 ? tool.successSchema : media.rest === void 0 ? void 0 : Schema.toCodecJson(media.rest);
|
|
84
|
+
const outputSchema = structured === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(nativeToolJsonSchema(structured, false)).pipe(Effect.orDie);
|
|
85
|
+
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(nativeToolJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
|
|
86
|
+
const liftedFieldNames = new Set(media?.fields.map(({ name }) => name));
|
|
87
|
+
const resultWithLiftedMedia = ({ encodedResult, result }) => {
|
|
88
|
+
if (media === void 0) return new McpSchema.CallToolResult({
|
|
89
|
+
isError: false,
|
|
90
|
+
structuredContent: encodedResult,
|
|
91
|
+
content: nativeJsonTextOf(encodedResult)
|
|
92
|
+
});
|
|
93
|
+
const blocks = liftedFrom(media, result).map(contentBlockOf);
|
|
94
|
+
if (structured === void 0) return new McpSchema.CallToolResult({
|
|
95
|
+
isError: false,
|
|
96
|
+
content: blocks
|
|
97
|
+
});
|
|
98
|
+
const encodedRestOfStruct = Predicate.isObject(encodedResult) ? Object.fromEntries(Object.entries(encodedResult).filter(([key]) => !liftedFieldNames.has(key))) : encodedResult;
|
|
99
|
+
return new McpSchema.CallToolResult({
|
|
100
|
+
isError: false,
|
|
101
|
+
structuredContent: encodedRestOfStruct,
|
|
102
|
+
content: [...nativeJsonTextOf(encodedRestOfStruct), ...blocks]
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
tool: new McpSchema.Tool({
|
|
107
|
+
name,
|
|
108
|
+
description: Tool.getDescription(tool),
|
|
109
|
+
inputSchema,
|
|
110
|
+
outputSchema,
|
|
111
|
+
annotations: {
|
|
112
|
+
...Context.getOption(tool.annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
|
|
113
|
+
readOnlyHint: Context.get(tool.annotations, Tool.Readonly),
|
|
114
|
+
destructiveHint: Context.get(tool.annotations, Tool.Destructive),
|
|
115
|
+
idempotentHint: Context.get(tool.annotations, Tool.Idempotent),
|
|
116
|
+
openWorldHint: Context.get(tool.annotations, Tool.OpenWorld)
|
|
117
|
+
},
|
|
118
|
+
_meta: Context.getOrUndefined(tool.annotations, Tool.Meta)
|
|
119
|
+
}),
|
|
120
|
+
annotations: tool.annotations,
|
|
121
|
+
handle: (payload) => built.handle(name, payload ?? {}, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => !result.isFailure ? Effect.succeed(resultWithLiftedMedia(result)) : result.failureOrigin === "handler" ? Effect.succeed(new McpSchema.CallToolResult({
|
|
122
|
+
isError: true,
|
|
123
|
+
content: nativeJsonTextOf(result.encodedResult)
|
|
124
|
+
})) : Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")))), Effect.catchCause(invalidParamsOrInternal), Effect.provideContext(retainedServices))
|
|
125
|
+
};
|
|
126
|
+
}));
|
|
127
|
+
for (const registration of registrations) yield* registry.addTool(registration);
|
|
128
|
+
});
|
|
55
129
|
const server = (apps, transport, features = Layer.empty, authentication) => {
|
|
56
|
-
const binding = bindTools(apps, authentication === void 0 ?
|
|
57
|
-
...
|
|
130
|
+
const binding = bindTools(apps, authentication === void 0 ? strictToolProjection : {
|
|
131
|
+
...strictToolProjection,
|
|
58
132
|
handler: (run, action) => (input) => action.caller === Anyone ? run(input) : recordStepUp(promote(authentication, run(input)))
|
|
59
133
|
});
|
|
60
|
-
|
|
61
|
-
|
|
134
|
+
const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
|
|
135
|
+
assertToolShapes(lifted);
|
|
136
|
+
const lifts = new Map(lifted.map(([{ name }, lift]) => [name, lift]));
|
|
62
137
|
const register = Effect.gen(function* () {
|
|
63
138
|
const registry = yield* McpServer.McpServer;
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
139
|
+
const namesClaimedByFeatures = registry.tools.flatMap(({ tool }) => lifts.has(tool.name) ? [tool.name] : []);
|
|
140
|
+
if (namesClaimedByFeatures.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${namesClaimedByFeatures.join(", ")}, claimed by an action and a native feature`));
|
|
66
141
|
const handlers = yield* Layer.build(binding.layer);
|
|
67
|
-
yield*
|
|
142
|
+
yield* registerToolsLiftingMedia(binding.toolkit, lifts).pipe(Effect.setContext(Context.add(handlers, McpServer.McpServer, registry)));
|
|
68
143
|
});
|
|
69
144
|
return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
|
|
70
145
|
};
|
|
71
|
-
|
|
72
|
-
* What an anonymous caller of a mixed endpoint may send besides a public tool's call:
|
|
73
|
-
* discovering the server and its tools, which the contracts describe, and cancelling.
|
|
74
|
-
* Everything else, a native feature's listing, completion or subscription among them, and
|
|
75
|
-
* any method a later revision adds, authenticates.
|
|
76
|
-
*/
|
|
77
|
-
const anonymous = /* @__PURE__ */ new Set([
|
|
146
|
+
const anonymousMethods = /* @__PURE__ */ new Set([
|
|
78
147
|
"server/discover",
|
|
79
148
|
"tools/list",
|
|
80
149
|
"notifications/cancelled"
|
|
81
150
|
]);
|
|
82
|
-
/**
|
|
83
|
-
* Whether a request to an endpoint serving `actions` must authenticate before the native
|
|
84
|
-
* server reads its body. An endpoint whose every tool is protected authenticates every
|
|
85
|
-
* request, so a host signs in when it connects. Otherwise the request's routing headers
|
|
86
|
-
* decide, which the native server refuses when they disagree with the body: discovery and a
|
|
87
|
-
* call of a public tool authenticate only a caller presenting credentials, and everything
|
|
88
|
-
* else authenticates, so native features are protected too. A request whose headers name
|
|
89
|
-
* no public tool as it is authenticates: it fails closed.
|
|
90
|
-
*/
|
|
91
151
|
const requiresAuthentication = (actions) => {
|
|
92
152
|
const open = new Set(actions.filter(({ caller }) => caller === Anyone).map(({ name }) => name));
|
|
93
153
|
return (headers) => {
|
|
94
154
|
if (open.size === 0) return true;
|
|
155
|
+
if (headers["mcp-protocol-version"] !== httpProtocol.protocolVersion) return true;
|
|
95
156
|
const method = headers["mcp-method"];
|
|
96
157
|
if (method === void 0) return true;
|
|
97
|
-
if (
|
|
158
|
+
if (anonymousMethods.has(method)) return false;
|
|
98
159
|
if (method !== "tools/call") return true;
|
|
99
160
|
const name = headers["mcp-name"];
|
|
100
161
|
return name === void 0 || !open.has(name);
|
|
101
162
|
};
|
|
102
163
|
};
|
|
164
|
+
const namesItsMethod = (request) => Effect.match(request.json, {
|
|
165
|
+
onFailure: () => true,
|
|
166
|
+
onSuccess: (body) => !Predicate.hasProperty(body, "method") || body.method === request.headers["mcp-method"]
|
|
167
|
+
});
|
|
103
168
|
function layerHttp(apps, { actions, features, path, authentication, ...options }) {
|
|
104
169
|
const selected = select(toList(apps), actions);
|
|
105
170
|
const served = selected.flatMap((app) => app.actions);
|
|
@@ -107,19 +172,20 @@ function layerHttp(apps, { actions, features, path, authentication, ...options }
|
|
|
107
172
|
const transport = server(selected, McpServer.layerHttp({
|
|
108
173
|
...options,
|
|
109
174
|
path: path ?? "/mcp",
|
|
110
|
-
protocols
|
|
175
|
+
protocols,
|
|
176
|
+
allowSessionTermination: true
|
|
111
177
|
}), features, authentication);
|
|
112
178
|
if (authentication === void 0) return transport;
|
|
113
179
|
const required = requiresAuthentication(served);
|
|
114
|
-
const middleware = HttpRouter.middleware(Effect.map(authentication
|
|
180
|
+
const middleware = HttpRouter.middleware(Effect.map(providerOf(authentication), (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method !== "POST" || required(request.headers) ? runtime.http(route, false) : Effect.flatMap(namesItsMethod(request), (named) => runtime.http(route, named)))));
|
|
115
181
|
return transport.pipe(Layer.provide(middleware.layer));
|
|
116
182
|
}
|
|
117
183
|
function runStdio(apps, { actions, features, ...options }) {
|
|
118
184
|
const transport = server(select(toList(apps), actions), McpServer.layerStdio({
|
|
119
185
|
...options,
|
|
120
|
-
protocols
|
|
186
|
+
protocols
|
|
121
187
|
}), features);
|
|
122
|
-
return Layer.launch(transport).pipe(
|
|
188
|
+
return Layer.launch(transport).pipe(withStderrConsole, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
|
|
123
189
|
}
|
|
124
190
|
//#endregion
|
|
125
191
|
export { layerHttp, runStdio };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Anyone, Certain, ErrorsOf } from "./contract/rules.mjs";
|
|
2
|
+
import { BuiltIn, BuiltIns } from "./contract/errors.mjs";
|
|
3
|
+
import { BuildError, Holding, Known, Member, MiddlewareOf, Protected, Selected, Served, Serving } from "./contract/implementation.mjs";
|
|
4
|
+
import { Call } from "./contract/call.mjs";
|
|
5
|
+
import { Action_d_exports, Any as Any$1, Errors } from "./Action.mjs";
|
|
6
|
+
import { Any as Any$2, DescriptorOf, Identity, Matching, PublicOnly, RemoteRequest, Required, ServedProvider, ServerBuildServices, VerifierError } from "./authentication/provider.mjs";
|
|
7
|
+
import { Context, Effect, Layer, Schema, Scope, Types } from "effect";
|
|
8
|
+
import { HttpRouter } from "effect/http";
|
|
9
|
+
import { Rpc, RpcClient, RpcClientError, RpcGroup, RpcMiddleware, RpcServer } from "effect/rpc";
|
|
10
|
+
//#region src/rpc/ActionRpc.d.ts
|
|
11
|
+
type BindingError = Errors[number];
|
|
12
|
+
type ErrorOf<A extends Any$1, E extends Errors, D> = Schema.Union<ReadonlyArray<A["error"][number] | E[number] | VerifierError<A, D> | BuiltIns>>;
|
|
13
|
+
type RpcOf<A extends Any$1, E extends Errors, D> = A extends Any$1 ? Rpc.Rpc<A["name"], A["input"], A["success"], ErrorOf<A, E, D>> : never;
|
|
14
|
+
/**
|
|
15
|
+
* An RPC binding: actions, the errors every rpc declares, and the descriptor authenticating
|
|
16
|
+
* its protected ones. Plain data, so a copy of it, or one made by another installed copy of
|
|
17
|
+
* this package, serves the same; `layer` serves it and `client` calls it.
|
|
18
|
+
*/
|
|
19
|
+
export interface Binding<Actions extends ReadonlyArray<Any$1>, E extends Errors = [], D extends Any$2 | undefined = undefined> {
|
|
20
|
+
readonly authentication: D;
|
|
21
|
+
/** The exact actions bound to this binding. */
|
|
22
|
+
readonly actions: Actions;
|
|
23
|
+
/** The errors every rpc declares besides its action's own, as a list. */
|
|
24
|
+
readonly error: E;
|
|
25
|
+
/**
|
|
26
|
+
* The native group of one rpc per action, named after it, as clients see it:
|
|
27
|
+
* `RpcClient.make(Rpc.group)` is the native client.
|
|
28
|
+
*/
|
|
29
|
+
readonly group: RpcGroup.RpcGroup<RpcOf<Actions[number], E, D>>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Any RPC binding. Its group is any native group here, since native groups are invariant in
|
|
33
|
+
* their rpcs; `actions` carries the types.
|
|
34
|
+
*/
|
|
35
|
+
export interface Any {
|
|
36
|
+
readonly authentication?: Any$2 | undefined;
|
|
37
|
+
readonly actions: ReadonlyArray<Any$1>;
|
|
38
|
+
readonly error: Errors;
|
|
39
|
+
readonly group: RpcGroup.Any;
|
|
40
|
+
}
|
|
41
|
+
/** Contract-level configuration shared by servers and clients. */
|
|
42
|
+
export interface Options<E extends Errors = Errors> {
|
|
43
|
+
/** Layer middleware's errors, which every rpc declares: one schema, or a list. */
|
|
44
|
+
readonly error?: E | E[number];
|
|
45
|
+
readonly authentication?: Any$2;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Bind actions once, for servers and clients alike, each as an rpc named after it, taking
|
|
49
|
+
* its action's input and answering its success. Every rpc declares its action's errors, the
|
|
50
|
+
* binding's `error`, and the built-in `InvalidInput`, `Unauthenticated` and `Forbidden`, so
|
|
51
|
+
* clients decode each as a typed failure; a protected one also declares what its descriptor's
|
|
52
|
+
* verifier fails with. Browser-safe, as an HTTP binding is.
|
|
53
|
+
* Guide: `docs/ActionRpc.md` in this package; in an application: `examples/live-rpc/`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function make<const Actions extends ReadonlyArray<Any$1 & {
|
|
56
|
+
readonly caller: typeof Anyone;
|
|
57
|
+
}>>(actions: Actions): Binding<Actions, [], undefined>;
|
|
58
|
+
export declare function make<const Actions extends ReadonlyArray<Any$1>, const O extends Options = {}>(actions: Actions, options: O & Required<Actions[number]> & Matching<Actions[number], NoInfer<DescriptorOf<O>>> & NoInfer<Known<O, Options>>): Binding<Actions, ErrorsOf<O>, DescriptorOf<O>>;
|
|
59
|
+
export declare function make<const Actions extends ReadonlyArray<Any$1>>(actions: Actions & PublicOnly<Actions>): Binding<Actions, [], undefined>;
|
|
60
|
+
type Middleware = ReadonlyArray<RpcMiddleware.AnyService>;
|
|
61
|
+
type IdOf<K> = K extends Context.Key<infer I extends RpcMiddleware.AnyId, unknown> ? I : never;
|
|
62
|
+
type Through<M extends Middleware, R> = number extends M["length"] ? R | RpcMiddleware.Requires<IdOf<M[number]>> : M extends readonly [infer K, ...infer Rest extends Middleware] ? Through<Rest, true extends Types.IsUnion<IdOf<K>> ? R | RpcMiddleware.Requires<IdOf<K>> : RpcMiddleware.ApplyServices<IdOf<K>, R>> : R;
|
|
63
|
+
type ForClient<K> = K extends {
|
|
64
|
+
readonly requiredForClient: infer F;
|
|
65
|
+
} ? true extends F ? K : never : never;
|
|
66
|
+
type ServerOnly<M extends Middleware, E extends Errors> = [Exclude<RpcMiddleware.Error<IdOf<M[number]>>, Extract<Certain<E>, BindingError>["Type"] | BuiltIn> | ForClient<M[number]>] extends [never] ? unknown : {
|
|
67
|
+
readonly "Layer middleware fails only with the binding's errors and needs no client": never;
|
|
68
|
+
};
|
|
69
|
+
type LayerRequest<App, A extends Any$1, M extends Middleware> = Exclude<Through<M, RemoteRequest<App, A>>, [Extract<Serving<App, A>, {
|
|
70
|
+
readonly caller: typeof Anyone;
|
|
71
|
+
}>] extends [never] ? Identity<Protected<Serving<App, A>>> : never>;
|
|
72
|
+
type RpcLayer<B extends Any, Apps extends Served, M extends Middleware, A extends Any$1 = B["actions"][number]> = Layer.Layer<never, BuildError<Holding<Member<Apps>, A>, A>, ServerBuildServices<Holding<Member<Apps>, A>, A> | RpcServer.Protocol | HttpRouter.Request.From<"Requires", LayerRequest<Member<Apps>, A, M>> | ServedProvider<Member<Apps>, A, B extends {
|
|
73
|
+
readonly authentication: infer D;
|
|
74
|
+
} ? D : never> | IdOf<M[number]>>;
|
|
75
|
+
/** `ActionRpc.layer`'s options. */
|
|
76
|
+
export interface LayerOptions<M extends Middleware = [], A extends Any$1 = never> {
|
|
77
|
+
/**
|
|
78
|
+
* The actions it serves, among the binding's actions the implementations hold: `[GetUser]`.
|
|
79
|
+
* Defaults to every one of them. A layer whose middleware requires the identity lists
|
|
80
|
+
* protected actions only.
|
|
81
|
+
*/
|
|
82
|
+
readonly actions?: ReadonlyArray<A> | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Native rpc middleware every served rpc runs, the first innermost: inside the
|
|
85
|
+
* authentication of a protected rpc, outside its decoding. Server-only: the binding's group,
|
|
86
|
+
* which clients use, carries none. A middleware requiring the identity needs a layer
|
|
87
|
+
* serving only protected actions.
|
|
88
|
+
*/
|
|
89
|
+
readonly middleware?: M;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Serve the binding's actions among `implementations` with Effect's own `RpcServer`, or those
|
|
93
|
+
* its `actions` lists, as `ActionHttp.layer` selects them. The host provides the protocol,
|
|
94
|
+
* such as `RpcServer.layerProtocolWebsocket` or `layerProtocolHttp` and its JSON serialization:
|
|
95
|
+
* the layer refuses to build under a protocol that does not speak JSON. A protected rpc
|
|
96
|
+
* authenticates each message from its headers with the binding's descriptor, outermost; then
|
|
97
|
+
* the layer's `middleware` runs, then the input is decoded, a failure answering `InvalidInput`,
|
|
98
|
+
* then the implementation's `authorize` and the handler, as on every surface.
|
|
99
|
+
*
|
|
100
|
+
* The server serves its own copy of the binding's group, whose payloads, successes and errors
|
|
101
|
+
* are the actions' JSON and which carries no native middleware: the protocol decodes a payload before
|
|
102
|
+
* any middleware runs, and answers one that does not decode with a defect, so each handler
|
|
103
|
+
* runs the authentication and the middleware itself, then decodes the input. A defect is
|
|
104
|
+
* logged with its cause, reported to the call's `ErrorReporter`s, and answered as its
|
|
105
|
+
* request's generic failure, never failing the connection's other calls; a declared failure is
|
|
106
|
+
* the caller's answer, and not reported.
|
|
107
|
+
*
|
|
108
|
+
* The layer fails as the builders do, needs at startup what they need, and per request what
|
|
109
|
+
* each implementation's authorization and the handlers of the actions it serves need, as
|
|
110
|
+
* router request markers, until its `middleware` provides them: over a router-mounted
|
|
111
|
+
* protocol, the route's request context does too. The identity of a protected action is
|
|
112
|
+
* provided by its authentication alone.
|
|
113
|
+
*/
|
|
114
|
+
export declare function layer<const B extends Any, const Apps extends Served>(binding: B, implementations: Apps): RpcLayer<B, Apps, []>;
|
|
115
|
+
export declare function layer<const B extends Any, const Apps extends Served, const O extends LayerOptions<Middleware, B["actions"][number]>>(binding: B, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O, Middleware>, B["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): RpcLayer<B, Apps, MiddlewareOf<O, Middleware>, Selected<O, B["actions"][number]>>;
|
|
116
|
+
/**
|
|
117
|
+
* What one call of an action `A` fails with, beside the binding's errors `E`: its declared and
|
|
118
|
+
* built-in errors, and Effect's own `RpcClientError`. Input that does not encode is
|
|
119
|
+
* `InvalidInput`, and nothing is sent; an answer that does not decode is a defect, as the
|
|
120
|
+
* native client makes it.
|
|
121
|
+
*/
|
|
122
|
+
export type MethodError<A extends Any$1, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | RpcClientError.RpcClientError;
|
|
123
|
+
type Method<A extends Any$1, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E>>>;
|
|
124
|
+
/**
|
|
125
|
+
* Every action of the binding `B`, as `client.<action>(input)`: a protected one may also fail
|
|
126
|
+
* with what its descriptor's verifier declares.
|
|
127
|
+
*/
|
|
128
|
+
export type Client<B extends Any> = { readonly [A in B["actions"][number] as A["name"]]: Method<A, B["error"][number] | VerifierError<A, B["authentication"]>>; };
|
|
129
|
+
/**
|
|
130
|
+
* The native `RpcClient.make` options `client` takes: `spanPrefix`, `spanAttributes`,
|
|
131
|
+
* `generateRequestId` and `disableTracing`. `flatten` changes the client's shape, which the
|
|
132
|
+
* method types cannot follow.
|
|
133
|
+
*/
|
|
134
|
+
export type ClientOptions = Omit<NonNullable<Parameters<typeof RpcClient.make>[1]>, "flatten">;
|
|
135
|
+
/**
|
|
136
|
+
* Effect's native `RpcClient` for a binding, one method per action taking the action's input
|
|
137
|
+
* directly: `client.greet({ name })`, as `ActionHttp.client`'s. The argument may be omitted
|
|
138
|
+
* when `{}` is a valid input. Requires the native client `Protocol` and a `Scope`, as
|
|
139
|
+
* `RpcClient.make` does; headers set with `RpcClient.withHeaders` around a call go with its
|
|
140
|
+
* message. The native client itself stays available: `RpcClient.make(Rpc.group)`.
|
|
141
|
+
*
|
|
142
|
+
* A call fails with a declared error value (the action's own, the binding's, a built-in
|
|
143
|
+
* `InvalidInput`, `Unauthenticated` or `Forbidden`, or on a protected rpc its descriptor's),
|
|
144
|
+
* or with Effect's own `RpcClientError` when the server could not be reached.
|
|
145
|
+
*/
|
|
146
|
+
export declare function client<const B extends Any>(binding: B, options?: ClientOptions): Effect.Effect<Client<B>, never, RpcClient.Protocol | Scope.Scope>;
|
|
147
|
+
//#endregion
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { InvalidInput } from "./contract/errors.mjs";
|
|
2
|
+
import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./contract/rules.mjs";
|
|
3
|
+
import { acquire, provideHandlers, servedBy, toList } from "./contract/implementation.mjs";
|
|
4
|
+
import { checked } from "./contract/call.mjs";
|
|
5
|
+
import { assertAuthentication, providerOf, withVerifierErrors } from "./authentication/provider.mjs";
|
|
6
|
+
import { Cause, Effect, ErrorReporter, Layer, Predicate, Result, Schema, SchemaTransformation } from "effect";
|
|
7
|
+
import { HttpClientRequest, HttpServerRequest } from "effect/http";
|
|
8
|
+
import { HttpApiBuilder } from "effect/http-api";
|
|
9
|
+
import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer } from "effect/rpc";
|
|
10
|
+
//#region src/rpc/ActionRpc.ts
|
|
11
|
+
const declaredErrorsOf = (action, errors, authentication) => projectedErrors(action, withVerifierErrors(action, errors, authentication?.error));
|
|
12
|
+
function make(actions, options = {}) {
|
|
13
|
+
assertOnce("action", actions);
|
|
14
|
+
const errors = errorList(options.error);
|
|
15
|
+
assertOwnTags("ActionRpc binding", errors);
|
|
16
|
+
assertAuthentication(actions, options.authentication);
|
|
17
|
+
return {
|
|
18
|
+
actions,
|
|
19
|
+
error: errors,
|
|
20
|
+
group: RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
|
|
21
|
+
payload: action.input,
|
|
22
|
+
success: action.success,
|
|
23
|
+
error: Schema.Union(declaredErrorsOf(action, errors, options.authentication))
|
|
24
|
+
}))),
|
|
25
|
+
authentication: options.authentication
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const credentialFromRpcHeaders = (security, headers) => HttpApiBuilder.securityDecode(security).pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, HttpServerRequest.fromClientRequest(HttpClientRequest.get("/", { headers }))), Effect.provideService(HttpServerRequest.ParsedSearchParams, {}));
|
|
29
|
+
const authenticationStepOf = (authentication) => Effect.map(providerOf(authentication), (provider) => (effect, { headers }) => credentialFromRpcHeaders(authentication.security, headers).pipe(Effect.flatMap(provider.authenticate), Effect.flatMap((actor) => Effect.provideService(effect, authentication.identity, actor))));
|
|
30
|
+
var InternalError = class extends Error {
|
|
31
|
+
[ErrorReporter.ignore] = true;
|
|
32
|
+
constructor() {
|
|
33
|
+
super("Internal server error");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var DeclaredFailure = class {
|
|
37
|
+
json;
|
|
38
|
+
[ErrorReporter.ignore] = true;
|
|
39
|
+
constructor(json) {
|
|
40
|
+
this.json = json;
|
|
41
|
+
}
|
|
42
|
+
toString() {
|
|
43
|
+
return Predicate.isString(this.json) ? this.json : JSON.stringify(this.json);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const DeclaredFromJson = Schema.Json.pipe(Schema.decodeTo(Schema.instanceOf(DeclaredFailure), SchemaTransformation.transform({
|
|
47
|
+
decode: (json) => new DeclaredFailure(json),
|
|
48
|
+
encode: (failure) => failure.json
|
|
49
|
+
})));
|
|
50
|
+
const callBoundaryOf = (error) => {
|
|
51
|
+
const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(error));
|
|
52
|
+
const internal = (cause) => Effect.logError(cause).pipe(Effect.andThen(ErrorReporter.report(cause)), Effect.andThen(Effect.die(new InternalError())));
|
|
53
|
+
return (effect) => Effect.catchCause(effect, (cause) => {
|
|
54
|
+
if (Cause.hasDies(cause)) return internal(cause);
|
|
55
|
+
const failure = Cause.findFail(cause);
|
|
56
|
+
return Result.isSuccess(failure) ? encode(failure.success.error).pipe(Effect.matchCauseEffect({
|
|
57
|
+
onFailure: (unencoded) => internal(Cause.combine(cause, unencoded)),
|
|
58
|
+
onSuccess: (encoded) => Effect.fail(new DeclaredFailure(encoded))
|
|
59
|
+
})) : Effect.failCause(cause);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const handlerOf = (action, run, steps, error) => {
|
|
63
|
+
const decodeStrictly = Schema.decodeUnknownEffect(Schema.toCodecJson(action.input), {
|
|
64
|
+
errors: "all",
|
|
65
|
+
onExcessProperty: "error"
|
|
66
|
+
});
|
|
67
|
+
const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.success));
|
|
68
|
+
const boundary = callBoundaryOf(error);
|
|
69
|
+
return (payload, options) => {
|
|
70
|
+
const call = Effect.suspend(() => decodeStrictly(payload)).pipe(Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(run), Effect.flatMap(encode));
|
|
71
|
+
const wrapped = steps.reduce((inner, step) => Effect.suspend(() => step(inner, {
|
|
72
|
+
...options,
|
|
73
|
+
payload
|
|
74
|
+
})), call);
|
|
75
|
+
return boundary(wrapped);
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
function layer(binding, served, options = {}) {
|
|
79
|
+
const apps = servedBy("RPC binding", binding.actions, toList(served), options.actions);
|
|
80
|
+
const actions = apps.flatMap((app) => app.actions);
|
|
81
|
+
assertAuthentication(actions, binding.authentication);
|
|
82
|
+
const middleware = options.middleware ?? [];
|
|
83
|
+
assertDistinct("middleware", middleware, (key) => key.key);
|
|
84
|
+
const auth = actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0;
|
|
85
|
+
const jsonGroup = RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
|
|
86
|
+
payload: Schema.Json,
|
|
87
|
+
success: Schema.Json,
|
|
88
|
+
error: DeclaredFromJson
|
|
89
|
+
})));
|
|
90
|
+
const handlers = Layer.unwrap(Effect.gen(function* () {
|
|
91
|
+
const { codecFor } = yield* RpcServer.Protocol;
|
|
92
|
+
if (codecFor !== RpcSerialization.json.codecFor) return yield* Effect.die(/* @__PURE__ */ new Error("ActionRpc serves a protocol speaking JSON: RpcSerialization.layerJson, layerNdjson, layerJsonRpc or layerNdJsonRpc, not another codec, such as schema-binary's"));
|
|
93
|
+
const listedMiddleware = yield* Effect.forEach(middleware, (key) => Effect.service(key));
|
|
94
|
+
const authenticate = auth === void 0 ? void 0 : yield* authenticationStepOf(auth);
|
|
95
|
+
const bound = yield* acquire(apps);
|
|
96
|
+
const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, handlerOf(action, run, action.caller === Anyone || authenticate === void 0 ? listedMiddleware : [...listedMiddleware, authenticate], Schema.Union(declaredErrorsOf(action, binding.error, binding.authentication)))]));
|
|
97
|
+
return jsonGroup.toLayer(byName);
|
|
98
|
+
})).pipe(provideHandlers(apps));
|
|
99
|
+
return RpcServer.layer(jsonGroup, { disableFatalDefects: true }).pipe(Layer.provide(handlers));
|
|
100
|
+
}
|
|
101
|
+
function client(binding, options) {
|
|
102
|
+
const group = binding.group;
|
|
103
|
+
return Effect.map(RpcClient.make(group, options), (client) => {
|
|
104
|
+
const native = client;
|
|
105
|
+
return Object.fromEntries(binding.actions.map((action) => {
|
|
106
|
+
const method = native[action.name];
|
|
107
|
+
if (method === void 0) throw new Error(`No client method for ${action.name}`);
|
|
108
|
+
return [action.name, checked(action, (payload) => method(payload))];
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { client, layer, make };
|