@gjermundgaraba/effect-actions 0.18.2 → 0.19.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 +20 -47
- package/dist/Action.d.mts +96 -47
- package/dist/Action.mjs +11 -14
- package/dist/ActionCli.d.mts +81 -42
- package/dist/ActionCli.mjs +52 -33
- package/dist/ActionHttp.d.mts +84 -43
- package/dist/ActionHttp.mjs +2 -2
- package/dist/ActionMcp.d.mts +54 -27
- package/dist/ActionMcp.mjs +5 -5
- package/dist/ActionRpc.d.mts +48 -35
- package/dist/ActionRpc.mjs +1 -1
- package/dist/ActionToolkit.d.mts +32 -13
- package/dist/ActionToolkit.mjs +10 -7
- package/dist/Authentication.d.mts +92 -40
- package/dist/Authentication.mjs +37 -35
- package/dist/Testing.d.mts +22 -24
- package/dist/authentication/provider.d.mts +1 -4
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -1
- package/dist/cli/commands.d.mts +34 -11
- package/dist/cli/commands.mjs +47 -22
- package/dist/contract/caller.mjs +1 -1
- package/dist/contract/declared.mjs +27 -14
- package/dist/contract/errors.d.mts +42 -17
- package/dist/contract/errors.mjs +42 -17
- package/dist/contract/hints.mjs +3 -2
- package/dist/contract/implementation.d.mts +5 -8
- package/dist/contract/implementation.mjs +29 -17
- package/dist/contract/media.d.mts +6 -2
- package/dist/contract/media.mjs +6 -2
- package/dist/contract/names.mjs +14 -12
- package/dist/contract/status.mjs +3 -10
- package/dist/http/client.d.mts +15 -4
- package/dist/http/client.mjs +1 -1
- package/dist/mcp/client.d.mts +21 -8
- package/dist/mcp/client.mjs +6 -8
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +167 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -37
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +3 -3
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +1 -1
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- package/docs/guarantees.md +0 -44
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -7,9 +7,9 @@ import { McpSchema, McpServer } from "effect/ai";
|
|
|
7
7
|
import { HttpRouter } from "effect/http";
|
|
8
8
|
//#region src/mcp/ActionMcp.d.ts
|
|
9
9
|
/**
|
|
10
|
-
* An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option but `protocols`,
|
|
11
|
+
* the server information, `instructions` and `extensions` among them, the actions `A` that are
|
|
12
|
+
* its tools, and the native features it serves beside them.
|
|
13
13
|
*/
|
|
14
14
|
export interface Options<E = never, R = never, A extends Any = Any> extends Omit<Parameters<typeof McpServer.layerStdio>[0], "protocols"> {
|
|
15
15
|
/**
|
|
@@ -20,7 +20,14 @@ export interface Options<E = never, R = never, A extends Any = Any> extends Omit
|
|
|
20
20
|
/**
|
|
21
21
|
* Native MCP features served beside the actions' tools, such as `McpServer.resource`,
|
|
22
22
|
* `McpServer.prompt` and `McpServer.toolkit` layers merged into one. They register on this
|
|
23
|
-
* server's registry
|
|
23
|
+
* server's registry and run with the services they were built with, and no `authorize` runs for
|
|
24
|
+
* them.
|
|
25
|
+
*
|
|
26
|
+
* **Gotchas**
|
|
27
|
+
*
|
|
28
|
+
* - Merged beside the endpoint instead of passed here, they register on another registry and are never served, without an error.
|
|
29
|
+
* - A service provided to this layer itself is built again for the features alone, so a resource reads another store than the tools write: provide it around the endpoint.
|
|
30
|
+
* - They never receive a request's services, so they cannot read the caller: content that depends on the caller is an action.
|
|
24
31
|
*/
|
|
25
32
|
readonly features?: Layer.Layer<never, E, R> | undefined;
|
|
26
33
|
}
|
|
@@ -32,6 +39,21 @@ export interface Options<E = never, R = never, A extends Any = Any> extends Omit
|
|
|
32
39
|
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"> {
|
|
33
40
|
/** The endpoint's route; defaults to `/mcp`. */
|
|
34
41
|
readonly path?: HttpRouter.PathInput;
|
|
42
|
+
/**
|
|
43
|
+
* The descriptor the protected tools' contracts name, whose provider, built by
|
|
44
|
+
* `Authentication.layer`, the routes provide. On an endpoint of protected tools only, every
|
|
45
|
+
* request authenticates before it is decoded; on a mixed one, anyone may discover the server,
|
|
46
|
+
* list its tools, cancel, and call a public tool in a 2026-07-28 request, and everything else
|
|
47
|
+
* authenticates first.
|
|
48
|
+
*
|
|
49
|
+
* **Gotchas**
|
|
50
|
+
*
|
|
51
|
+
* - A credential a request presents is still verified, on a public tool's call too: an expired or invalid one answers 401, its challenge naming `invalid_token`, on which a client refreshes it or signs in again.
|
|
52
|
+
* - `tools/list` names every tool, protected ones included: serve public tools on an endpoint of their own if signed-out callers must not see the rest.
|
|
53
|
+
* - A client of a 2025 revision opens a session, which on an endpoint serving a protected tool only a signed-in caller does: it signs in for every request there, a public tool's call and `tools/list` included.
|
|
54
|
+
* - Every prompt and resource request needs a credential, listings included: to offer them signed out, serve them on an endpoint without protected tools.
|
|
55
|
+
* - A handler that sends a notification starts the response, so a refusal after it is an `isError: true` tool result, not the 401 or 403 a client re-authorizes on: refuse in `authorize`.
|
|
56
|
+
*/
|
|
35
57
|
readonly authentication?: Any$1;
|
|
36
58
|
/**
|
|
37
59
|
* Whether MCP 2026-07-28 clients may subscribe, holding a stream open for list changes and
|
|
@@ -43,9 +65,10 @@ export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> ext
|
|
|
43
65
|
/**
|
|
44
66
|
* `true` runs a server per request, as Effect's `McpServer.layerHttpRequestScoped` does, for a
|
|
45
67
|
* host such as Cloudflare Workers where nothing may outlive a request. It speaks MCP 2026-07-28
|
|
46
|
-
* alone, so a client that opens with `initialize` is refused
|
|
47
|
-
* no subscriptions, so `allowSubscriptions` is ignored,
|
|
48
|
-
* list-change notification
|
|
68
|
+
* alone, so a client that opens with `initialize` is refused; it keeps no session and takes only
|
|
69
|
+
* `POST`, a `DELETE` answering 405. It has no subscriptions, so `allowSubscriptions` is ignored,
|
|
70
|
+
* and native `features` send no list-change notification. `McpServer.elicit` reaches no client
|
|
71
|
+
* on it, as Effect's `McpServer.layerHttpRequestScoped` says.
|
|
49
72
|
*/
|
|
50
73
|
readonly requestScoped?: boolean | undefined;
|
|
51
74
|
}
|
|
@@ -61,34 +84,38 @@ type Inferring<D, A extends Any> = [D] extends [undefined] ? [Protected<A>] exte
|
|
|
61
84
|
* Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28, 2025-11-25 or
|
|
62
85
|
* 2025-06-18, as the client negotiates. A 2026-07-28 request stands alone; a client of a 2025
|
|
63
86
|
* revision opens a session with `initialize`, which holds what it sent there and no identity,
|
|
64
|
-
* and lasts until that client ends it with a DELETE, answered 204, or the process stops.
|
|
65
|
-
* serverless host, see `requestScoped`.
|
|
66
|
-
*
|
|
67
|
-
* Protected tools need the `authentication` descriptor their contracts name, and the
|
|
68
|
-
* provider `Authentication.layer` builds from it. On an endpoint of protected tools only,
|
|
69
|
-
* every request authenticates before it is decoded. On a mixed endpoint, anyone may discover
|
|
70
|
-
* the server, list its tools, cancel, and call a public tool in a 2026-07-28 request;
|
|
71
|
-
* everything else authenticates first, a session's every request included, so a protected
|
|
72
|
-
* tool's description is listed to anyone but run only for its caller.
|
|
73
|
-
* On a Bearer endpoint, a protected tool's call refused with `Unauthenticated`, or with
|
|
74
|
-
* `Forbidden` naming scopes, is answered with its HTTP status and challenge, 401 or 403, as MCP
|
|
75
|
-
* authorization requires, unless its response has started; any other failure is a tool result. Claude Code connects with
|
|
76
|
-
* `claude mcp add --transport http <name> <url>`. In a tool call, what middleware provides per request wins over
|
|
77
|
-
* what the endpoint was built with; native `features` read only what they were built with,
|
|
78
|
-
* never a request's. Never provide an identity at startup: a tool call takes its identity
|
|
79
|
-
* only from the request it authenticated.
|
|
87
|
+
* and lasts until that client ends it with a DELETE, answered 204, or the process stops. Protected
|
|
88
|
+
* tools need `authentication`; for a serverless host, see `requestScoped`. In a tool call, what
|
|
89
|
+
* middleware provides per request wins over what the endpoint was built with.
|
|
80
90
|
* Guide: `docs/ActionMcp.md` in this package; in an application: `examples/mcp-tools/`.
|
|
91
|
+
*
|
|
92
|
+
* **Gotchas**
|
|
93
|
+
*
|
|
94
|
+
* - A success is `isError: false` with `structuredContent` its JSON and the same JSON as one text block, both `null` for an action without `success`; an `Action.Image` field is left out of `structuredContent` and its text, its block following them, and a success of images alone has no `structuredContent`.
|
|
95
|
+
* - A declared error is an `isError: true` tool result in a 200 holding its JSON as one text block, with no `structuredContent`; a refusal is answered as `Action.Unauthenticated` and `Action.Forbidden` say.
|
|
96
|
+
* - Arguments that do not decode never reach the handler: an `isError: true` result in a 200, text `Invalid parameters for tool '<name>': …`, or for a 2025-06-18 client a JSON-RPC error.
|
|
97
|
+
* - A defect, an undeclared failure, or a success or an error that does not encode is `isError: true`, text `Tool execution failed due to an internal server error.`: the cause goes to the log and to the call's `ErrorReporter`s, never to the client.
|
|
98
|
+
* - An unknown tool is a JSON-RPC error `-32602` in a 200; a body that is not JSON a JSON-RPC error `-32700` in a 200; a `Content-Type` missing or not JSON an empty 415; another HTTP method an empty 405 with `Allow: POST, DELETE`, `Allow: POST` where `requestScoped`, or the 401 of an endpoint with authentication when signed out.
|
|
99
|
+
* - A 2025-11-25 or 2025-06-18 client gets `structuredContent` only for a success that is an object: any other success arrives as its text block alone, a string unquoted.
|
|
100
|
+
* - A client asking for an earlier revision, such as 2025-03-26, is offered 2025-11-25, so a client speaking only that earlier one cannot connect.
|
|
101
|
+
* - A request body is read whole, however large, and a 2025 session holds what its `initialize` sent until it ends, so an oversized `initialize` costs memory for that long: limit bodies as `ActionHttp.layer` says.
|
|
81
102
|
*/
|
|
82
103
|
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>>;
|
|
83
104
|
/**
|
|
84
105
|
* Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
|
|
85
106
|
* 2026-07-28, 2025-11-25 or 2025-06-18, as the host negotiates: the whole
|
|
86
107
|
* program of an MCP subprocess, which succeeds once the host closes its side and every call
|
|
87
|
-
* in flight has been answered. A signal interrupts it.
|
|
108
|
+
* in flight has been answered. A signal interrupts it. Its own Effect logs and `Console` output
|
|
109
|
+
* go to stderr, since stdout carries the protocol. The host supplies the `Stdio` service, and
|
|
110
|
+
* provides the identity around `runStdio`; arguments are tool input only and never establish
|
|
111
|
+
* request identity or authority, and each implementation's `authorize` runs for its protected
|
|
112
|
+
* actions. Its tool results, and how it answers each revision and an earlier one asked for, are
|
|
113
|
+
* `layerHttp`'s.
|
|
114
|
+
*
|
|
115
|
+
* **Gotchas**
|
|
88
116
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* request identity or authority; each implementation's `authorize` runs for its protected actions.
|
|
117
|
+
* - A line that is not JSON goes unanswered, with no `-32700` as over HTTP, and the lines after it are still answered.
|
|
118
|
+
* - The layers provided around it log to stdout, among the protocol's lines: apply `ActionCli.logToStderr` to the program, last, as its JSDoc says.
|
|
92
119
|
*/
|
|
93
120
|
export declare function runStdio<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never>(implementations: Apps, options: Options<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<E, R, Any>>>): Effect.Effect<void, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Stdio.Stdio | ToolRequestContext<ServedRequest<Member<Apps>, SelectedOf<O, Apps>>> | Features<R>>;
|
|
94
121
|
//#endregion
|
package/dist/ActionMcp.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Anyone, assertAuthentication } from "./contract/caller.mjs";
|
|
2
|
-
import { assertOnce } from "./contract/names.mjs";
|
|
2
|
+
import { assertOnce, heldOnce } from "./contract/names.mjs";
|
|
3
3
|
import { projectedErrors } from "./contract/declared.mjs";
|
|
4
4
|
import { hintsOf } from "./contract/hints.mjs";
|
|
5
5
|
import { acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
|
|
@@ -26,9 +26,9 @@ const toolJsonSchema = (schema, strict) => {
|
|
|
26
26
|
};
|
|
27
27
|
const assertToolShapes = (lifted) => {
|
|
28
28
|
const inputs = lifted.map(([action]) => action).filter((action) => !isToolArgumentsSchema(toolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
|
|
29
|
-
if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
|
|
29
|
+
if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")} (wrap a union, an array or a scalar in a field, omit input for no arguments, or leave the action out of actions)`);
|
|
30
30
|
const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
|
|
31
|
-
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(", ")}`);
|
|
31
|
+
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(", ")} (make the image such a field, or give an optional array as a required one, empty when absent; an image in input or an error is not served over MCP, so leave that action out of actions)`);
|
|
32
32
|
};
|
|
33
33
|
const nativeInternalErrorText = "Tool execution failed due to an internal server error.";
|
|
34
34
|
const textErrorResult = (text) => new McpSchema.CallToolResult({
|
|
@@ -118,13 +118,13 @@ const registerTools = Effect.fnUntraced(function* (apps, authentication) {
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
const server = (apps, transport, features = Layer.empty, authentication) => {
|
|
121
|
-
assertOnce("MCP tool", apps.flatMap((app) => app.actions));
|
|
121
|
+
assertOnce("MCP tool", apps.flatMap((app) => app.actions), heldOnce);
|
|
122
122
|
const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
|
|
123
123
|
assertToolShapes(lifted);
|
|
124
124
|
const names = new Set(lifted.map(([{ name }]) => name));
|
|
125
125
|
const register = Effect.gen(function* () {
|
|
126
126
|
const namesClaimedByFeatures = (yield* McpServer.McpServer).tools.flatMap(({ tool }) => names.has(tool.name) ? [tool.name] : []);
|
|
127
|
-
if (namesClaimedByFeatures.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${namesClaimedByFeatures.join(", ")}, claimed by an action and a native feature`));
|
|
127
|
+
if (namesClaimedByFeatures.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${namesClaimedByFeatures.join(", ")}, claimed by an action and a native feature (rename the native tool, or leave the action out of actions)`));
|
|
128
128
|
yield* registerTools(apps, authentication);
|
|
129
129
|
});
|
|
130
130
|
return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
|
package/dist/ActionRpc.d.mts
CHANGED
|
@@ -18,6 +18,7 @@ type RpcOf<A extends Any$1, E extends Errors, D> = A extends Any$1 ? Rpc.Rpc<A["
|
|
|
18
18
|
* this package, serves the same; `layer` serves it and `client` calls it.
|
|
19
19
|
*/
|
|
20
20
|
export interface Binding<Actions extends ReadonlyArray<Any$1>, E extends Errors = [], D extends Any$2 | undefined = undefined> {
|
|
21
|
+
/** The descriptor authenticating its protected actions, or `undefined` when it names none. */
|
|
21
22
|
readonly authentication: D;
|
|
22
23
|
/** The exact actions bound to this binding. */
|
|
23
24
|
readonly actions: Actions;
|
|
@@ -41,8 +42,15 @@ export interface Any {
|
|
|
41
42
|
}
|
|
42
43
|
/** Contract-level configuration shared by servers and clients. */
|
|
43
44
|
export interface Options<E extends Errors = Errors> {
|
|
44
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* What the layer's `middleware` fails with, which every rpc declares so clients decode it:
|
|
47
|
+
* one schema, or a list.
|
|
48
|
+
*/
|
|
45
49
|
readonly error?: E | E[number];
|
|
50
|
+
/**
|
|
51
|
+
* `Authentication.make(...)` of the protected actions' identity, which authenticates each of
|
|
52
|
+
* their messages: required when the binding holds a protected action, and of its identity.
|
|
53
|
+
*/
|
|
46
54
|
readonly authentication?: Any$2;
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
@@ -77,48 +85,50 @@ type RpcLayer<B extends Any, Apps extends Served, M extends Middleware, A extend
|
|
|
77
85
|
export interface LayerOptions<M extends Middleware = [], A extends Any$1 = never> {
|
|
78
86
|
/**
|
|
79
87
|
* The actions it serves, among the binding's actions the implementations hold: `[GetUser]`.
|
|
80
|
-
* Defaults to every one of them.
|
|
81
|
-
* protected actions only.
|
|
88
|
+
* Defaults to every one of them.
|
|
82
89
|
*/
|
|
83
90
|
readonly actions?: ReadonlyArray<A> | undefined;
|
|
84
91
|
/**
|
|
85
|
-
* Native rpc middleware every served rpc runs, the first innermost: inside
|
|
86
|
-
* authentication of a protected rpc,
|
|
87
|
-
*
|
|
88
|
-
*
|
|
92
|
+
* Native rpc middleware every served rpc runs on each message, the first innermost: inside
|
|
93
|
+
* the authentication of a protected rpc, so none provides what a verifier reads, and outside
|
|
94
|
+
* its decoding, so it also sees input that does not decode. Server-only: the binding's group,
|
|
95
|
+
* which clients use, carries none.
|
|
96
|
+
*
|
|
97
|
+
* **Gotchas**
|
|
98
|
+
*
|
|
99
|
+
* - A middleware requiring the identity leaves it owed on a layer serving a public action: bind the protected actions apart, or read the identity with `Effect.serviceOption`, which a public rpc's call lacks.
|
|
100
|
+
* - A middleware provides a service only to those listed before it and the handlers, and only in a list written in place as a tuple: listed earlier, or in an array-typed list, it leaves the service owed.
|
|
101
|
+
* - Router middleware around the layer runs once per WebSocket, at its upgrade, so its value holds for the whole connection: what differs per message needs a listed middleware.
|
|
89
102
|
*/
|
|
90
103
|
readonly middleware?: M;
|
|
91
104
|
}
|
|
92
105
|
/**
|
|
93
106
|
* Serve the binding's actions among `implementations` with Effect's own `RpcServer`, or those
|
|
94
|
-
* its `actions` lists, as `ActionHttp.layer` selects them
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* the
|
|
99
|
-
*
|
|
107
|
+
* its `actions` lists, as `ActionHttp.layer` selects them, under the protocol and JSON
|
|
108
|
+
* serialization the host provides. Each message of a protected rpc authenticates from its own
|
|
109
|
+
* headers with the binding's descriptor; then the listed `middleware` runs, the input decodes,
|
|
110
|
+
* and the implementation's `authorize` and the handler run, as on every surface. The layer
|
|
111
|
+
* fails as the builders do and needs at startup what they need; per request it owes, as router
|
|
112
|
+
* request markers, what the authorizers and handlers read until its `middleware` provides it,
|
|
113
|
+
* or, over a router-mounted protocol, the route's request context.
|
|
114
|
+
* A protected action's identity comes from its authentication alone.
|
|
100
115
|
*
|
|
101
|
-
*
|
|
102
|
-
* are the actions' JSON and which carries no native middleware: the protocol decodes a payload before
|
|
103
|
-
* any middleware runs, and answers one that does not decode with a defect, so each handler
|
|
104
|
-
* runs the authentication and the middleware itself, then decodes the input. A defect is
|
|
105
|
-
* logged with its cause, reported to the call's `ErrorReporter`s, and answered as its
|
|
106
|
-
* request's generic failure, never failing the connection's other calls; a declared failure is
|
|
107
|
-
* the caller's answer, and not reported.
|
|
116
|
+
* **Gotchas**
|
|
108
117
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
118
|
+
* - Provide the protocol to this layer, inside what `HttpRouter.serve` serves: `RpcServer.layerProtocolWebsocket` and `layerProtocolHttp` add their route to that router, and provided around `HttpRouter.serve` they leave `HttpRouter` owed.
|
|
119
|
+
* - An rpc the layer does not serve ends its call with `Unknown request tag`, as does every rpc of a second `ActionRpc.layer` given the same protocol, which never receives a message: serve a protocol's rpcs from one layer, or give the second a protocol on another path.
|
|
120
|
+
* - A declared error, or `InvalidInput` for input that does not decode, is the call's typed failure; a refusal is answered as `Action.Unauthenticated` and `Action.Forbidden` say; the socket stays open, so a token may expire under it and a client handles `Unauthenticated` itself.
|
|
121
|
+
* - A defect, an undeclared failure, a middleware that throws, or a success or an error that does not encode ends that call with an `Error` whose message is `Internal server error`, its cause logged and reported to the call's `ErrorReporter`s; the connection's other calls go on.
|
|
122
|
+
* - An `ErrorReporter` is given only what ends a call with `Internal server error`, never a declared failure, a refusal or `InvalidInput`, unlike `ActionHttp.layer`'s `Info`: an alert counting declared failures counts none from rpcs.
|
|
123
|
+
* - An rpc's credential is read from its headers alone: an API key `in: "query"` never reaches authentication.
|
|
124
|
+
* - Over a protocol with no router, such as `RpcServer.layerProtocolSocketServer` or `layerProtocolStdio`, there is no HTTP request or route: a provider publishing a `protectedResource` leaves `HttpRouter` owed, and a verifier yielding a request service leaves it owed.
|
|
114
125
|
*/
|
|
115
126
|
export declare function layer<const B extends Any, const Apps extends Served>(binding: B, implementations: Apps): RpcLayer<B, Apps, []>;
|
|
116
127
|
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]>>;
|
|
117
128
|
/**
|
|
118
129
|
* What one call of an action `A` fails with, beside the binding's errors `E`: its declared and
|
|
119
|
-
* built-in errors, and Effect's own `RpcClientError
|
|
120
|
-
*
|
|
121
|
-
* native client makes it.
|
|
130
|
+
* built-in errors, and Effect's own `RpcClientError` when the server could not be reached. An
|
|
131
|
+
* answer that does not decode is a defect, as the native client makes it.
|
|
122
132
|
*/
|
|
123
133
|
export type MethodError<A extends Any$1, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | RpcClientError.RpcClientError;
|
|
124
134
|
type Method<A extends Any$1, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E>>>;
|
|
@@ -135,14 +145,17 @@ export type Client<B extends Any> = { readonly [A in B["actions"][number] as A["
|
|
|
135
145
|
export type ClientOptions = Omit<NonNullable<Parameters<typeof RpcClient.make>[1]>, "flatten">;
|
|
136
146
|
/**
|
|
137
147
|
* Effect's native `RpcClient` for a binding, one method per action taking the action's input
|
|
138
|
-
* directly: `client.greet({ name })`, as `ActionHttp.client`'s
|
|
139
|
-
*
|
|
140
|
-
* `RpcClient.make` does; headers set with `RpcClient.withHeaders`
|
|
141
|
-
* message. The native client
|
|
148
|
+
* directly: `client.greet({ name })`, as `ActionHttp.client`'s, the argument omitted when `{}`
|
|
149
|
+
* is a valid input, each call failing with its `MethodError`. Requires the native client
|
|
150
|
+
* `Protocol` and a `Scope`, as `RpcClient.make` does; headers set with `RpcClient.withHeaders`
|
|
151
|
+
* around a call go with its message. The native client stays available:
|
|
152
|
+
* `RpcClient.make(Rpc.group)`.
|
|
153
|
+
*
|
|
154
|
+
* **Gotchas**
|
|
142
155
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
156
|
+
* - It drops undeclared input fields as it encodes, and fails with `InvalidInput`, sending nothing, for an input that does not encode.
|
|
157
|
+
* - A browser WebSocket cannot set an `Authorization` header on its upgrade, so under Bearer a protected call without its own token fails with `Unauthenticated`, `A bearer token is required.`: send the token on each call with `RpcClient.withHeaders`.
|
|
158
|
+
* - The socket belongs to the Layer providing the protocol, not to the `Scope` around the client: provided around one call, it opens a socket for that call alone.
|
|
146
159
|
*/
|
|
147
160
|
export declare function client<const B extends Any>(binding: B, options?: ClientOptions): Effect.Effect<Client<B>, never, RpcClient.Protocol | Scope.Scope>;
|
|
148
161
|
//#endregion
|
package/dist/ActionRpc.mjs
CHANGED
|
@@ -93,7 +93,7 @@ function client(binding, options) {
|
|
|
93
93
|
const native = client;
|
|
94
94
|
return Object.fromEntries(binding.actions.map((action) => {
|
|
95
95
|
const method = native[action.name];
|
|
96
|
-
if (method === void 0) throw new Error(`No client method for ${action.name}`);
|
|
96
|
+
if (method === void 0) throw new Error(`No client method for ${action.name} (use the binding \`ActionRpc.make\` returned, whose group holds this action's rpc)`);
|
|
97
97
|
return [action.name, checked(action, (payload) => method(payload))];
|
|
98
98
|
}));
|
|
99
99
|
});
|
package/dist/ActionToolkit.d.mts
CHANGED
|
@@ -17,6 +17,12 @@ type ToolFor<App, Listed extends Any> = App extends unknown ? Serving<App, Liste
|
|
|
17
17
|
* implementations: the `layer` of a `make` call serves the tools it selected in any `toolkit`
|
|
18
18
|
* of the same implementations, while two implementations never run each other's handlers,
|
|
19
19
|
* even with tools of one name.
|
|
20
|
+
*
|
|
21
|
+
* **Gotchas**
|
|
22
|
+
*
|
|
23
|
+
* - Two `make` calls of one implementation share their handlers: their `layer`s, given different startup services and provided together, serve both toolkits with one of them; use separate implementations for separate services.
|
|
24
|
+
* - Another implementation's `layer` with tools of the same names type-checks, and the first call dies with Effect's `Cannot read properties of undefined (reading 'context')`: provide the `layer` the same `make` returned.
|
|
25
|
+
* - `handle(name, arguments)` on `yield* toolkit` takes the JSON a model sends; its handler starts when `handle` runs, so the caller is provided around the whole `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, never around the stream alone.
|
|
20
26
|
*/
|
|
21
27
|
export interface Tools<T extends Record<string, Tool.Any>, E, R> {
|
|
22
28
|
/**
|
|
@@ -28,7 +34,7 @@ export interface Tools<T extends Record<string, Tool.Any>, E, R> {
|
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* One call of a tool, as `needsApproval` receives it: its action's name, the action, and
|
|
31
|
-
* the call's decoded input.
|
|
37
|
+
* the call's decoded input.
|
|
32
38
|
*/
|
|
33
39
|
export type ToolCall<A extends Any = Any> = A extends Any ? {
|
|
34
40
|
readonly name: A["name"];
|
|
@@ -39,28 +45,38 @@ export type ToolCall<A extends Any = Any> = A extends Any ? {
|
|
|
39
45
|
export interface Options<A extends Any> {
|
|
40
46
|
/**
|
|
41
47
|
* The actions that are tools, among the implementations' actions: `[GetUser, RenameUser]`.
|
|
42
|
-
* Each keeps its implementation's authorization and builder. Defaults to every action of them
|
|
48
|
+
* Each keeps its implementation's authorization and builder. Defaults to every action of them,
|
|
49
|
+
* one added to an implementation later included.
|
|
43
50
|
*/
|
|
44
51
|
readonly actions?: ReadonlyArray<A> | undefined;
|
|
45
52
|
/**
|
|
46
53
|
* Whether a model's call needs approval before it runs: a boolean, or an Effect of one, for
|
|
47
54
|
* each call, with Effect's native approval context. `LanguageModel` asks for approval
|
|
48
|
-
* instead of calling the tool when it is `true
|
|
49
|
-
*
|
|
55
|
+
* instead of calling the tool when it is `true`, and when the Effect fails, so a policy that
|
|
56
|
+
* cannot be read asks; a defect, a throw included, ends the turn as Effect's own does. The
|
|
57
|
+
* Effect runs in the caller's context and requires nothing, so it reads the caller with
|
|
58
|
+
* `Effect.serviceOption`. Defaults to none.
|
|
59
|
+
*
|
|
60
|
+
* **Gotchas**
|
|
61
|
+
*
|
|
62
|
+
* - `call` ranges over every action of the implementations, whatever `actions` lists: check `call.name` to narrow `action` and `input`.
|
|
63
|
+
* - Approval authorizes nothing, which stays `authorize`'s, and only a model's turn asks for it: `handle` and `ActionMcp` never do.
|
|
50
64
|
*/
|
|
51
|
-
readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean>;
|
|
65
|
+
readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean, unknown>;
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
54
68
|
* Project implementations into Effect's native AI toolkit: one tool per action, keyed by its
|
|
55
69
|
* name, of the listed `actions` or, without them, of every action of the implementations.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* when the resulting toolkit handles a call, the identity an implementation's `authorize`
|
|
60
|
-
* reads included: the caller provides it. `needsApproval` marks the calls a model must have
|
|
61
|
-
* approved; it authorizes nothing, which stays `authorize`'s.
|
|
62
|
-
* Guide: `docs/ActionToolkit.md` in this package; a loop served behind a route, with approval:
|
|
70
|
+
* Startup services build `layer`; request services, the identity `authorize` reads included,
|
|
71
|
+
* are provided around each turn or call the toolkit handles. Guide: `docs/ActionToolkit.md` in
|
|
72
|
+
* this package; a loop served behind a route, with approval:
|
|
63
73
|
* `examples/kitchen-sink/src/assistant/`.
|
|
74
|
+
*
|
|
75
|
+
* **Gotchas**
|
|
76
|
+
*
|
|
77
|
+
* - A success is a result with `isFailure: false`, `result` the value and `encodedResult` its JSON; a declared error a result with `isFailure: true` and `result` the error, which the model reads, never a raised failure; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say.
|
|
78
|
+
* - Arguments that do not decode are a result with `isFailure: true` whose `result` is an `AiError` with `reason` a `ToolParameterValidationError`, never `InvalidInput`, and neither `authorize` nor the handler runs; undeclared arguments are dropped and the call runs.
|
|
79
|
+
* - A defect, or a failure the action's errors reject, is the call's defect; a success or an error that does not encode fails the call with an `AiError`, `Failed to encode result for tool …`.
|
|
64
80
|
*/
|
|
65
81
|
export declare function make<const Apps extends Served, const O extends Options<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, Options<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<Any>>>>): Tools<{ readonly [T in ToolFor<Member<Apps>, Offered<O, ActionOf<Member<Apps>>>> as T["name"]]: T; }, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>;
|
|
66
82
|
/** A tool call a model asked to have approved, which no answer has resolved yet. */
|
|
@@ -70,7 +86,10 @@ export interface Approval {
|
|
|
70
86
|
readonly toolCallId: string;
|
|
71
87
|
/** The tool's name, the action's. */
|
|
72
88
|
readonly name: string;
|
|
73
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* The call's arguments as the model sent them: show them as JSON, and decode them with
|
|
91
|
+
* `Schema.toCodecJson(<action>.input)`, the tool's parameters, only where you read a field.
|
|
92
|
+
*/
|
|
74
93
|
readonly params: unknown;
|
|
75
94
|
}
|
|
76
95
|
/**
|
package/dist/ActionToolkit.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertOnce } from "./contract/names.mjs";
|
|
1
|
+
import { assertOnce, heldOnce } from "./contract/names.mjs";
|
|
2
2
|
import { projectedErrors } from "./contract/declared.mjs";
|
|
3
3
|
import { hintsOf } from "./contract/hints.mjs";
|
|
4
4
|
import { Implementation, acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
|
|
@@ -22,12 +22,15 @@ const jsonToolOf = (action) => Tool.make(action.name, {
|
|
|
22
22
|
function make(apps, options) {
|
|
23
23
|
const served = select(toList(apps), options?.actions);
|
|
24
24
|
const needsApproval = options?.needsApproval;
|
|
25
|
-
assertOnce("tool", served.flatMap((app) => app.actions));
|
|
26
|
-
const toolOf = (action, app) => Object.assign(needsApproval === void 0 ? jsonToolOf(action) : jsonToolOf(action).setNeedsApproval((input, context) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
assertOnce("tool", served.flatMap((app) => app.actions), heldOnce);
|
|
26
|
+
const toolOf = (action, app) => Object.assign(needsApproval === void 0 ? jsonToolOf(action) : jsonToolOf(action).setNeedsApproval((input, context) => {
|
|
27
|
+
const decided = needsApproval({
|
|
28
|
+
name: action.name,
|
|
29
|
+
action,
|
|
30
|
+
input
|
|
31
|
+
}, context);
|
|
32
|
+
return Effect.isEffect(decided) ? Effect.catch(decided, () => Effect.succeed(true)) : decided;
|
|
33
|
+
}), { id: `${Implementation.runKey(app)}/${action.name}` });
|
|
31
34
|
const toolkit = Toolkit.make(...served.flatMap((app) => app.actions.map((action) => annotateFromContract(toolOf(action, app), action))));
|
|
32
35
|
return {
|
|
33
36
|
toolkit,
|