@gjermundgaraba/effect-actions 0.10.0 → 0.11.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 +4 -5
- package/dist/Action.d.mts +3 -2
- package/dist/Action.mjs +2 -1
- package/dist/ActionMcp.d.mts +2 -2
- package/dist/ActionMcp.mjs +143 -19
- package/dist/Testing.d.mts +6 -4
- package/dist/Testing.mjs +39 -7
- package/dist/internal/mcp.d.mts +3 -0
- package/dist/internal/mcp.mjs +132 -2
- package/dist/internal/media.d.mts +8 -0
- package/dist/internal/media.mjs +26 -0
- package/dist/internal/schema-errors.mjs +3 -3
- package/docs/Action.md +27 -25
- package/docs/ActionCli.md +1 -0
- package/docs/ActionHttp.md +1 -0
- package/docs/ActionMcp.md +87 -2
- package/docs/ActionToolkit.md +2 -0
- package/docs/CONTEXT.md +1 -0
- package/docs/README.md +1 -1
- package/docs/Testing.md +2 -2
- package/docs/guarantees.md +13 -12
- package/docs/setup.md +11 -6
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -78,10 +78,10 @@ const greeting = Effect.gen(function* () {
|
|
|
78
78
|
## Install
|
|
79
79
|
|
|
80
80
|
```sh
|
|
81
|
-
pnpm add @gjermundgaraba/effect-actions effect@~4.0.
|
|
81
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.2
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Add `@effect/platform-node@~4.0.
|
|
84
|
+
Add `@effect/platform-node@~4.0.2` to serve or run a CLI from Node. Every module is a
|
|
85
85
|
subpath import (`.../Action`, `.../ActionHttp`, ...); there is no package root, so a
|
|
86
86
|
contracts-only bundle never loads a server.
|
|
87
87
|
|
|
@@ -107,9 +107,8 @@ release; in a project that installs the package, point the agent at
|
|
|
107
107
|
|
|
108
108
|
## Status
|
|
109
109
|
|
|
110
|
-
The `effect` peer
|
|
111
|
-
change in a minor release
|
|
112
|
-
`devDependencies` (see [docs/setup.md](docs/setup.md)). Actions are unary JSON over HTTP and MCP: no streaming, uploads, prompts, or resources.
|
|
110
|
+
The `effect` peer admits the patches of the Effect release the package is tested against, since
|
|
111
|
+
the modules the surfaces build on may change in a minor release (see [docs/setup.md](docs/setup.md#install)). Actions are unary JSON over HTTP and MCP: no streaming, uploads, prompts, or resources.
|
|
113
112
|
Token verification and the authorization rule belong to the application; the library supplies the authentication descriptor and provider, runs the rule, and answers the refusals. See [docs/setup.md](docs/setup.md).
|
|
114
113
|
|
|
115
114
|
## Acknowledgements
|
package/dist/Action.d.mts
CHANGED
|
@@ -2,9 +2,10 @@ import { Anyone } from "./internal/actions.mjs";
|
|
|
2
2
|
import { BuiltIn, BuiltIns, Forbidden, InvalidInput, Issue, Refusal, Unauthenticated } from "./internal/errors.mjs";
|
|
3
3
|
import { ActionOf, AnyImplementation, Authorize, BuildError, BuildServices, Holding, Implementation, Known, Member, Offered, OptionalUnless, Protected, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
|
|
4
4
|
import { Call } from "./internal/call.mjs";
|
|
5
|
+
import { Image } from "./internal/media.mjs";
|
|
5
6
|
import { Context, Effect, Layer, Schema, Scope } from "effect";
|
|
6
7
|
declare namespace Action_d_exports {
|
|
7
|
-
export { Action, Any, AnyImplementation, Anyone, Authorize, BuildError, BuildServices, BuiltIn, Client, ClientOptions, Errors, Forbidden, Handler, Handlers, Implementation, InvalidInput, Issue, Mcp, Options, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
|
|
8
|
+
export { Action, Any, AnyImplementation, Anyone, Authorize, BuildError, BuildServices, BuiltIn, Client, ClientOptions, Errors, Forbidden, Handler, Handlers, Image, Implementation, InvalidInput, Issue, Mcp, Options, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
|
|
8
9
|
}
|
|
9
10
|
/** Any service-free schema. Only handlers may require services. */
|
|
10
11
|
type Codec = Schema.Codec<unknown, unknown, never, never>;
|
|
@@ -298,4 +299,4 @@ export interface ClientOptions<A extends Any> {
|
|
|
298
299
|
*/
|
|
299
300
|
export declare function client<const Apps extends Served, const O extends ClientOptions<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, ClientOptions<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, ClientOptions<Any>>>>): Effect.Effect<Client<Apps, Offered<O, ActionOf<Member<Apps>>>>, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Scope.Scope>;
|
|
300
301
|
//#endregion
|
|
301
|
-
export { Action_d_exports, type AnyImplementation, Anyone, type Authorize, type BuildError, type BuildServices, BuiltIn, Forbidden, type Implementation, InvalidInput, type Issue, Refusal, Unauthenticated };
|
|
302
|
+
export { Action_d_exports, type AnyImplementation, Anyone, type Authorize, type BuildError, type BuildServices, BuiltIn, Forbidden, Image, type Implementation, InvalidInput, type Issue, Refusal, Unauthenticated };
|
package/dist/Action.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated } from "./in
|
|
|
2
2
|
import { Anyone, assertKnown, assertName, assertOnce, assertOwnTags, errorList, projectedErrors } from "./internal/actions.mjs";
|
|
3
3
|
import { inputOf } from "./internal/call.mjs";
|
|
4
4
|
import { Implementation, builders, built, memoized, select, toList } from "./internal/implementation.mjs";
|
|
5
|
+
import { Image } from "./internal/media.mjs";
|
|
5
6
|
import { Array as Array$1, Cause, Context, Effect, Predicate, Schema } from "effect";
|
|
6
7
|
//#region src/Action.ts
|
|
7
8
|
/**
|
|
@@ -130,4 +131,4 @@ function client(served, options) {
|
|
|
130
131
|
return Effect.map(built(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, methodOf(action, run)])));
|
|
131
132
|
}
|
|
132
133
|
//#endregion
|
|
133
|
-
export { Anyone, BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
|
|
134
|
+
export { Anyone, BuiltIn, Forbidden, Image, InvalidInput, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
|
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -74,8 +74,8 @@ export declare function layerHttp<const Apps extends Served, const O extends Sel
|
|
|
74
74
|
/**
|
|
75
75
|
* Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
|
|
76
76
|
* 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
|
|
77
|
+
* program of an MCP subprocess, which succeeds once the host closes its side and every call
|
|
78
|
+
* in flight has been answered. A signal interrupts it.
|
|
79
79
|
*
|
|
80
80
|
* Effect logs go to stderr, since stdout carries the protocol. The host supplies the
|
|
81
81
|
* `Stdio` service and the identity. Arguments are tool input only and never establish
|
package/dist/ActionMcp.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Anyone } from "./internal/actions.mjs";
|
|
2
2
|
import { provideHandlers, select, toList } from "./internal/implementation.mjs";
|
|
3
3
|
import { assertAuthentication, promote } from "./internal/authentication.mjs";
|
|
4
|
-
import { httpProtocol } from "./internal/mcp.mjs";
|
|
4
|
+
import { fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom } from "./internal/mcp.mjs";
|
|
5
5
|
import { recordStepUp } from "./internal/refusal.mjs";
|
|
6
6
|
import { logToStderr } from "./internal/console.mjs";
|
|
7
7
|
import { bindTools } from "./internal/tools.mjs";
|
|
8
|
-
import { Cause, Context, Effect, Exit, Fiber, JsonPointer, Layer, Predicate, Schema } from "effect";
|
|
8
|
+
import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, References, Result, Schema, Stream } from "effect";
|
|
9
9
|
import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
10
|
-
import { McpProtocol, McpSchema, McpServer, Tool } from "effect/ai";
|
|
10
|
+
import { AiError, McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
|
|
11
11
|
//#region src/ActionMcp.ts
|
|
12
12
|
/**
|
|
13
13
|
* The revisions served over stdio: 2026-07-28 and the stateful revisions hosts open stdio
|
|
@@ -23,8 +23,9 @@ const stdioProtocols = [
|
|
|
23
23
|
];
|
|
24
24
|
/**
|
|
25
25
|
* MCP has a JSON-only wire contract. Success is the encoded success as structured content,
|
|
26
|
-
* and its JSON as text
|
|
27
|
-
* refuses undeclared arguments and
|
|
26
|
+
* and its JSON as text, but for its media, each lifted into a block of its own; declared
|
|
27
|
+
* failures are returned as JSON text. The native server refuses undeclared arguments and
|
|
28
|
+
* publishes closed input schemas.
|
|
28
29
|
*/
|
|
29
30
|
const projection = {
|
|
30
31
|
label: "MCP tool",
|
|
@@ -33,38 +34,161 @@ const projection = {
|
|
|
33
34
|
/** Whether a JSON Schema is one the native server takes for a tool's arguments. */
|
|
34
35
|
const isToolJson = Schema.is(McpSchema.ToolJson);
|
|
35
36
|
/**
|
|
36
|
-
* The JSON Schema
|
|
37
|
-
* root resolved
|
|
37
|
+
* The JSON Schema of `schema`, a JSON codec, as the native server lists it for a tool: a
|
|
38
|
+
* `$ref` at its root resolved, as MCP requires an object root, and its definitions as `$defs`.
|
|
39
|
+
* A strict one refuses undeclared keys.
|
|
38
40
|
*/
|
|
39
|
-
const
|
|
40
|
-
const document = Schema.toJsonSchemaDocument(
|
|
41
|
+
const toolJsonSchema = (schema, strict) => {
|
|
42
|
+
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
|
|
41
43
|
const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
|
|
42
|
-
|
|
44
|
+
const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
|
|
45
|
+
return root === void 0 || Object.keys(document.definitions).length === 0 ? root : {
|
|
46
|
+
...root,
|
|
47
|
+
$defs: document.definitions
|
|
48
|
+
};
|
|
43
49
|
};
|
|
44
50
|
/**
|
|
45
|
-
* Refuse, when the server is made, the actions
|
|
46
|
-
*
|
|
51
|
+
* Refuse, when the server is made, the actions whose input is not one object, as the root of a
|
|
52
|
+
* tool's arguments must be, naming every one, reading the JSON Schema the native
|
|
47
53
|
* server reads: not a union, an array, a scalar, nor `Schema.Struct({})`, which takes any value
|
|
48
54
|
* but `null`, where the native server dies when the layer builds, suggesting
|
|
49
|
-
* `Tool.EmptyParams`.
|
|
55
|
+
* `Tool.EmptyParams`. Then refuse those holding media where no tool lifts it, as each action's
|
|
56
|
+
* `Lift` says.
|
|
50
57
|
*/
|
|
51
|
-
const assertShapes = (
|
|
52
|
-
const inputs =
|
|
58
|
+
const assertShapes = (lifted) => {
|
|
59
|
+
const inputs = lifted.map(([action]) => action).filter((action) => !isToolJson(toolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
|
|
53
60
|
if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
|
|
61
|
+
const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
|
|
62
|
+
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
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* What the native server answers a call that fails other than as its contract declares: a
|
|
66
|
+
* defect, an invalid success or error, or an unknown tool.
|
|
67
|
+
*/
|
|
68
|
+
const internalError = "Tool execution failed due to an internal server error.";
|
|
69
|
+
/** A tool's error result, `text` its one text block. */
|
|
70
|
+
const errorResult = (text) => new McpSchema.CallToolResult({
|
|
71
|
+
isError: true,
|
|
72
|
+
content: [{
|
|
73
|
+
type: "text",
|
|
74
|
+
text
|
|
75
|
+
}]
|
|
76
|
+
});
|
|
77
|
+
/** The one text block holding `encoded`'s JSON, none for `undefined`, as the native server sends it. */
|
|
78
|
+
const textOf = (encoded) => encoded === void 0 ? [] : [{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: JSON.stringify(encoded)
|
|
81
|
+
}];
|
|
82
|
+
/** The content block a lifted media value is sent as, naming its field where it has one. */
|
|
83
|
+
const blockOf = ({ kind, field, value }) => ({
|
|
84
|
+
type: kind,
|
|
85
|
+
data: value.data,
|
|
86
|
+
mimeType: value.mimeType,
|
|
87
|
+
...field === void 0 ? {} : { _meta: { [fieldKey]: field } }
|
|
88
|
+
});
|
|
89
|
+
/** Why a tool call's arguments failed to decode, as the native server tells it, if they did. */
|
|
90
|
+
const invalidParameters = (error) => AiError.isAiError(error) && Predicate.isTagged(error.reason, "ToolParameterValidationError") ? error.reason : void 0;
|
|
91
|
+
const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
|
|
92
|
+
/**
|
|
93
|
+
* Register the tools of `toolkit` on the endpoint's registry, as `McpServer.registerToolkit`
|
|
94
|
+
* does, lifting the media `lifts` finds in a tool's success into blocks of their own. The native
|
|
95
|
+
* registration sends every success as structured content and one JSON text block, and takes
|
|
96
|
+
* no option, so this one follows it: its decode options, failure classification, defect
|
|
97
|
+
* scrubbing and listing, and the request services it leaves out of what it retains. A tool
|
|
98
|
+
* whose action holds no media is registered as the native one would be: a test compares their
|
|
99
|
+
* listings and results, not the services each call runs with.
|
|
100
|
+
*/
|
|
101
|
+
const registerTools = Effect.fnUntraced(function* (toolkit, lifts) {
|
|
102
|
+
const registry = yield* McpServer.McpServer;
|
|
103
|
+
const built = yield* toolkit.pipe(Effect.updateContext((context) => Context.merge(context, Context.mergeAll(...Object.values(toolkit.tools).flatMap((tool) => {
|
|
104
|
+
const key = Context.Service(tool.id);
|
|
105
|
+
return Option.toArray(Context.getOption(context, key)).map((handler) => Context.make(key, {
|
|
106
|
+
...handler,
|
|
107
|
+
context: omitRequestServices(handler.context)
|
|
108
|
+
}));
|
|
109
|
+
})))));
|
|
110
|
+
const services = omitRequestServices(yield* Effect.context());
|
|
111
|
+
const internalToolError = (cause) => {
|
|
112
|
+
const failure = Cause.findFail(cause);
|
|
113
|
+
return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(Effect.provideContext(ErrorReporter.report(cause), services)), Effect.as(errorResult(internalError)));
|
|
114
|
+
};
|
|
115
|
+
const handleCause = (cause) => {
|
|
116
|
+
const failure = Cause.findFail(cause);
|
|
117
|
+
if (Result.isSuccess(failure)) {
|
|
118
|
+
const { error } = failure.success;
|
|
119
|
+
const invalid = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin) === "parameters" ? invalidParameters(error) : void 0;
|
|
120
|
+
if (invalid !== void 0) return Effect.fail(new McpSchema.InvalidParams({ message: invalid.message }));
|
|
121
|
+
}
|
|
122
|
+
return internalToolError(cause);
|
|
123
|
+
};
|
|
124
|
+
const registrations = yield* Effect.forEach(Object.entries(built.tools), ([name, tool]) => Effect.gen(function* () {
|
|
125
|
+
const strict = Tool.getStrictMode(tool) === true;
|
|
126
|
+
const decodeOptions = {
|
|
127
|
+
onExcessProperty: strict ? "error" : "ignore",
|
|
128
|
+
errors: "all"
|
|
129
|
+
};
|
|
130
|
+
const lift = lifts.get(name);
|
|
131
|
+
const media = lift === void 0 || lift.fields.length === 0 ? void 0 : lift;
|
|
132
|
+
const structured = media === void 0 ? tool.successSchema : media.rest === void 0 ? void 0 : Schema.toCodecJson(media.rest);
|
|
133
|
+
const outputSchema = structured === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(toolJsonSchema(structured, false)).pipe(Effect.orDie);
|
|
134
|
+
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(toolJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
|
|
135
|
+
const lifted = new Set(media?.fields.map(({ name }) => name));
|
|
136
|
+
const succeeded = ({ encodedResult, result }) => {
|
|
137
|
+
if (media === void 0) return new McpSchema.CallToolResult({
|
|
138
|
+
isError: false,
|
|
139
|
+
structuredContent: encodedResult,
|
|
140
|
+
content: textOf(encodedResult)
|
|
141
|
+
});
|
|
142
|
+
const blocks = liftedFrom(media, result).map(blockOf);
|
|
143
|
+
if (structured === void 0) return new McpSchema.CallToolResult({
|
|
144
|
+
isError: false,
|
|
145
|
+
content: blocks
|
|
146
|
+
});
|
|
147
|
+
const rest = Predicate.isObject(encodedResult) ? Object.fromEntries(Object.entries(encodedResult).filter(([key]) => !lifted.has(key))) : encodedResult;
|
|
148
|
+
return new McpSchema.CallToolResult({
|
|
149
|
+
isError: false,
|
|
150
|
+
structuredContent: rest,
|
|
151
|
+
content: [...textOf(rest), ...blocks]
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
tool: new McpSchema.Tool({
|
|
156
|
+
name,
|
|
157
|
+
description: Tool.getDescription(tool),
|
|
158
|
+
inputSchema,
|
|
159
|
+
outputSchema,
|
|
160
|
+
annotations: {
|
|
161
|
+
...Context.getOption(tool.annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
|
|
162
|
+
readOnlyHint: Context.get(tool.annotations, Tool.Readonly),
|
|
163
|
+
destructiveHint: Context.get(tool.annotations, Tool.Destructive),
|
|
164
|
+
idempotentHint: Context.get(tool.annotations, Tool.Idempotent),
|
|
165
|
+
openWorldHint: Context.get(tool.annotations, Tool.OpenWorld)
|
|
166
|
+
},
|
|
167
|
+
_meta: Context.getOrUndefined(tool.annotations, Tool.Meta)
|
|
168
|
+
}),
|
|
169
|
+
annotations: tool.annotations,
|
|
170
|
+
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(succeeded(result)) : result.failureOrigin === "handler" ? Effect.succeed(new McpSchema.CallToolResult({
|
|
171
|
+
isError: true,
|
|
172
|
+
content: textOf(result.encodedResult)
|
|
173
|
+
})) : Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")))), Effect.catchCause(handleCause), Effect.provideContext(services))
|
|
174
|
+
};
|
|
175
|
+
}));
|
|
176
|
+
for (const registration of registrations) yield* registry.addTool(registration);
|
|
177
|
+
});
|
|
55
178
|
const server = (apps, transport, features = Layer.empty, authentication) => {
|
|
56
179
|
const binding = bindTools(apps, authentication === void 0 ? projection : {
|
|
57
180
|
...projection,
|
|
58
181
|
handler: (run, action) => (input) => action.caller === Anyone ? run(input) : recordStepUp(promote(authentication, run(input)))
|
|
59
182
|
});
|
|
60
|
-
|
|
61
|
-
|
|
183
|
+
const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
|
|
184
|
+
assertShapes(lifted);
|
|
185
|
+
const lifts = new Map(lifted.map(([{ name }, lift]) => [name, lift]));
|
|
62
186
|
const register = Effect.gen(function* () {
|
|
63
187
|
const registry = yield* McpServer.McpServer;
|
|
64
|
-
const claimed = registry.tools.flatMap(({ tool }) =>
|
|
188
|
+
const claimed = registry.tools.flatMap(({ tool }) => lifts.has(tool.name) ? [tool.name] : []);
|
|
65
189
|
if (claimed.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${claimed.join(", ")}, claimed by an action and a native feature`));
|
|
66
190
|
const handlers = yield* Layer.build(binding.layer);
|
|
67
|
-
yield*
|
|
191
|
+
yield* registerTools(binding.toolkit, lifts).pipe(Effect.setContext(Context.add(handlers, McpServer.McpServer, registry)));
|
|
68
192
|
});
|
|
69
193
|
return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
|
|
70
194
|
};
|
package/dist/Testing.d.mts
CHANGED
|
@@ -71,10 +71,12 @@ export type McpClient<Actions extends ReadonlyArray<Any>> = { readonly [A in Act
|
|
|
71
71
|
/**
|
|
72
72
|
* A client of an MCP endpoint served by `ActionMcp.layerHttp`, one method per action calling
|
|
73
73
|
* its tool with one stateless request, as `ActionHttp.client` calls routes: the input is
|
|
74
|
-
* encoded with the action's schema, and the success decoded from its structured content
|
|
75
|
-
*
|
|
76
|
-
* is
|
|
77
|
-
*
|
|
74
|
+
* encoded with the action's schema, and the success decoded from its structured content, each
|
|
75
|
+
* media field from the blocks whose `_meta` names it, and a success that is media, or whose
|
|
76
|
+
* every field is, from its blocks alone. A declared error the tool returns, the action's own
|
|
77
|
+
* or a refusal, is its decoded value, and so is a refusal the endpoint's authentication
|
|
78
|
+
* answers with. The argument may be omitted when `{}` is a valid input. Requires the native
|
|
79
|
+
* `HttpClient`, such as the one `layer` provides.
|
|
78
80
|
*/
|
|
79
81
|
export declare function mcpClient<const Actions extends ReadonlyArray<Any>>(actions: Actions, options?: McpClientOptions): Effect.Effect<McpClient<Actions>, never, HttpClient.HttpClient>;
|
|
80
82
|
/**
|
package/dist/Testing.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Refusal } from "./internal/errors.mjs";
|
|
2
2
|
import { assertOnce, projectedErrors } from "./internal/actions.mjs";
|
|
3
3
|
import { inputOf } from "./internal/call.mjs";
|
|
4
|
-
import { defaultPath, statelessRequest } from "./internal/mcp.mjs";
|
|
4
|
+
import { defaultPath, liftOf, statelessRequest } from "./internal/mcp.mjs";
|
|
5
5
|
import { clientOf } from "./internal/memory.mjs";
|
|
6
6
|
import { Context, Effect, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
|
|
7
7
|
import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
|
|
8
8
|
import { McpSchema } from "effect/ai";
|
|
9
|
-
import { Sse } from "effect/encoding";
|
|
9
|
+
import { Base64, Sse } from "effect/encoding";
|
|
10
10
|
//#region src/Testing.ts
|
|
11
11
|
function layer(routes) {
|
|
12
12
|
if (!Layer.isLayer(routes)) return clientOf(routes);
|
|
@@ -44,10 +44,41 @@ const replyOf = (response, text) => {
|
|
|
44
44
|
}).feed(text);
|
|
45
45
|
return Option.fromNullishOr(data.flatMap((message) => Option.toArray(decodeReply(message))).at(-1));
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* The JSON of the media `field` a tool result's blocks hold: those of its kind naming it, or
|
|
49
|
+
* none, for the whole success, in order; an array, or else the one, if any. Surplus blocks for
|
|
50
|
+
* one value stay an array, which its decoding refuses.
|
|
51
|
+
*/
|
|
52
|
+
const mediaOf = ({ name, kind, many }, result) => {
|
|
53
|
+
const values = result.content.flatMap((block) => block.type === kind && block._meta?.["effect-actions/field"] === name ? [{
|
|
54
|
+
data: Base64.encode(block.data),
|
|
55
|
+
mimeType: block.mimeType
|
|
56
|
+
}] : []);
|
|
57
|
+
return many || values.length > 1 ? values : values[0];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The encoded success of a tool result lifting the media `lift` finds: the whole success's
|
|
61
|
+
* blocks, or its structured content with each media field taken from its blocks alone, an
|
|
62
|
+
* empty one when nothing else was left.
|
|
63
|
+
*/
|
|
64
|
+
const reassembled = (lift, result) => {
|
|
65
|
+
const [whole] = lift.fields;
|
|
66
|
+
if (whole !== void 0 && whole.name === void 0) return mediaOf(whole, result);
|
|
67
|
+
const { structuredContent = {} } = result;
|
|
68
|
+
if (!Predicate.isObject(structuredContent)) return structuredContent;
|
|
69
|
+
const lifted = new Set(lift.fields.map(({ name }) => name));
|
|
70
|
+
return Object.fromEntries([...Object.entries(structuredContent).filter(([key]) => !lifted.has(key)), ...lift.fields.flatMap((field) => {
|
|
71
|
+
const value = mediaOf(field, result);
|
|
72
|
+
return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
|
|
73
|
+
})]);
|
|
74
|
+
};
|
|
47
75
|
/** Fail with the value of one of `errors` that `text` holds, or else with `otherwise`. */
|
|
48
76
|
const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
|
|
49
|
-
/**
|
|
50
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The tool call of `action`, whose success its tool lifts as `lift` says, with `input` on
|
|
79
|
+
* `client`, sending to `url`.
|
|
80
|
+
*/
|
|
81
|
+
const callTool = (client, url, action, lift, input) => {
|
|
51
82
|
const { name } = action;
|
|
52
83
|
const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
|
|
53
84
|
return Effect.gen(function* () {
|
|
@@ -69,8 +100,8 @@ const callTool = (client, url, action, input) => {
|
|
|
69
100
|
const error = textOf(result.content.find((block) => block.type === "text")) ?? "";
|
|
70
101
|
return yield* failWith(projectedErrors(action), error, other(`returned an error: ${error}`));
|
|
71
102
|
}
|
|
72
|
-
if (result.structuredContent === void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
|
|
73
|
-
return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(result
|
|
103
|
+
if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
|
|
104
|
+
return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(reassembled(lift, result));
|
|
74
105
|
});
|
|
75
106
|
};
|
|
76
107
|
function mcpClient(actions, { url = defaultPath, transformClient = identity } = {}) {
|
|
@@ -78,7 +109,8 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
|
|
|
78
109
|
return Effect.map(HttpClient.HttpClient, (native) => {
|
|
79
110
|
const client = transformClient(native);
|
|
80
111
|
return Object.fromEntries(actions.map((action) => {
|
|
81
|
-
|
|
112
|
+
const lift = liftOf(action.success);
|
|
113
|
+
return [action.name, (...args) => Effect.flatMap(inputOf(action, args), (input) => callTool(client, url, action, lift, input))];
|
|
82
114
|
}));
|
|
83
115
|
});
|
|
84
116
|
}
|
package/dist/internal/mcp.d.mts
CHANGED
package/dist/internal/mcp.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Media, isMarked, kindOf } from "./media.mjs";
|
|
2
|
+
import { Predicate, Schema, SchemaAST } from "effect";
|
|
2
3
|
import { McpProtocol } from "effect/ai";
|
|
3
4
|
//#region src/internal/mcp.ts
|
|
4
5
|
/**
|
|
@@ -48,5 +49,134 @@ const statelessRequest = (method, params) => {
|
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
51
|
};
|
|
52
|
+
/** The `_meta` key of a lifted block naming its field. */
|
|
53
|
+
const fieldKey = "effect-actions/field";
|
|
54
|
+
const isMedia = Schema.is(Media);
|
|
55
|
+
const isMediaArray = Schema.is(Schema.Array(Media));
|
|
56
|
+
/** The schema `ast` stands for, a `Schema.suspend` resolved, which only delays its definition. */
|
|
57
|
+
const resolved = (ast) => SchemaAST.isSuspend(ast) ? resolved(ast.thunk()) : ast;
|
|
58
|
+
/** The kind `ast` is, suspended or not. */
|
|
59
|
+
const kindIn = (ast) => kindOf(resolved(ast));
|
|
60
|
+
/**
|
|
61
|
+
* The media `ast` is, one value or an array of them, as a whole success may be: an array without
|
|
62
|
+
* an encoding of its own whose every element is media of one kind, such as `Schema.Array` or
|
|
63
|
+
* `Schema.NonEmptyArray` of it.
|
|
64
|
+
*/
|
|
65
|
+
const wholeOf = (suspended) => {
|
|
66
|
+
const ast = resolved(suspended);
|
|
67
|
+
const one = kindOf(ast);
|
|
68
|
+
if (one !== void 0) return {
|
|
69
|
+
kind: one,
|
|
70
|
+
many: false
|
|
71
|
+
};
|
|
72
|
+
if (!SchemaAST.isArrays(ast) || ast.encoding !== void 0) return void 0;
|
|
73
|
+
const [kind, ...others] = new Set([...ast.elements, ...ast.rest].map(kindIn));
|
|
74
|
+
return kind === void 0 || others.length > 0 ? void 0 : {
|
|
75
|
+
kind,
|
|
76
|
+
many: true
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The media a field's `ast` is: one value, which `Schema.optional` or `Schema.optionalKey` may
|
|
81
|
+
* make absent, or an array, which they may not, as no block would tell an absent one from an
|
|
82
|
+
* empty one.
|
|
83
|
+
*/
|
|
84
|
+
const fieldOf = (field) => {
|
|
85
|
+
const optional = field.context?.isOptional === true;
|
|
86
|
+
const ast = resolved(field);
|
|
87
|
+
if (optional && SchemaAST.isUnion(ast) && ast.types.length === 2) {
|
|
88
|
+
const [value, ...more] = ast.types.filter((member) => !SchemaAST.isUndefined(member));
|
|
89
|
+
const kind = value === void 0 || more.length > 0 ? void 0 : kindIn(value);
|
|
90
|
+
return kind === void 0 ? void 0 : {
|
|
91
|
+
kind,
|
|
92
|
+
many: false
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const whole = wholeOf(ast);
|
|
96
|
+
return whole === void 0 || optional && whole.many ? void 0 : whole;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* How an MCP tool sends `success`: it lifts the success itself, one value or an array, or the
|
|
100
|
+
* top-level media fields of a struct without an encoding of its own, keyed by strings, and
|
|
101
|
+
* sends the rest of the struct, if any is left.
|
|
102
|
+
*/
|
|
103
|
+
const liftOf = (success) => {
|
|
104
|
+
const ast = resolved(success.ast);
|
|
105
|
+
const whole = wholeOf(ast);
|
|
106
|
+
if (whole !== void 0) return {
|
|
107
|
+
fields: [{
|
|
108
|
+
name: void 0,
|
|
109
|
+
...whole
|
|
110
|
+
}],
|
|
111
|
+
rest: void 0
|
|
112
|
+
};
|
|
113
|
+
if (!SchemaAST.isObjects(ast) || ast.encoding !== void 0) return {
|
|
114
|
+
fields: [],
|
|
115
|
+
rest: success
|
|
116
|
+
};
|
|
117
|
+
const fields = ast.propertySignatures.flatMap(({ name, type }) => {
|
|
118
|
+
const field = fieldOf(type);
|
|
119
|
+
return field === void 0 ? [] : [{
|
|
120
|
+
name: String(name),
|
|
121
|
+
...field
|
|
122
|
+
}];
|
|
123
|
+
});
|
|
124
|
+
if (fields.length === 0) return {
|
|
125
|
+
fields,
|
|
126
|
+
rest: success
|
|
127
|
+
};
|
|
128
|
+
const names = new Set(fields.map(({ name }) => name));
|
|
129
|
+
const rest = ast.propertySignatures.filter(({ name }) => !names.has(name));
|
|
130
|
+
const { identifier: _identifier, ...annotations } = ast.annotations ?? {};
|
|
131
|
+
return {
|
|
132
|
+
fields,
|
|
133
|
+
rest: rest.length === 0 && ast.indexSignatures.length === 0 ? void 0 : Schema.make(new SchemaAST.Objects(rest, ast.indexSignatures, annotations))
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** The schemas `ast` is made of, its encoded side included. */
|
|
137
|
+
const partsOf = (ast) => [
|
|
138
|
+
...SchemaAST.isDeclaration(ast) ? ast.typeParameters : [],
|
|
139
|
+
...SchemaAST.isArrays(ast) ? [...ast.elements, ...ast.rest] : [],
|
|
140
|
+
...SchemaAST.isObjects(ast) ? [...ast.propertySignatures, ...ast.indexSignatures].map(({ type }) => type) : [],
|
|
141
|
+
...SchemaAST.isUnion(ast) ? ast.types : [],
|
|
142
|
+
...SchemaAST.isSuspend(ast) ? [ast.thunk()] : [],
|
|
143
|
+
...(ast.encoding ?? []).map(({ to }) => to)
|
|
144
|
+
];
|
|
145
|
+
/** Whether `ast` holds media anywhere, each schema visited once, so a recursive one ends. */
|
|
146
|
+
const holdsMedia = (ast) => {
|
|
147
|
+
const seen = /* @__PURE__ */ new Set();
|
|
148
|
+
const pending = [ast];
|
|
149
|
+
for (let next = pending.pop(); next !== void 0; next = pending.pop()) {
|
|
150
|
+
if (seen.has(next)) continue;
|
|
151
|
+
seen.add(next);
|
|
152
|
+
if (isMarked(next)) return true;
|
|
153
|
+
pending.push(...partsOf(next));
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Whether `action`, whose success an MCP tool lifts as `lift` says, holds media where no tool
|
|
159
|
+
* lifts it: in its input, in a declared error, or in the rest of its success, such as nested in
|
|
160
|
+
* a field or in a union.
|
|
161
|
+
*/
|
|
162
|
+
const isMisplaced = (action, { rest }) => {
|
|
163
|
+
return [
|
|
164
|
+
action.input,
|
|
165
|
+
...action.error,
|
|
166
|
+
...rest === void 0 ? [] : [rest]
|
|
167
|
+
].some(({ ast }) => holdsMedia(ast));
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* The media `lift` finds in `success`, a decoded value its schema has validated: the whole
|
|
171
|
+
* success's, or each field's, in field order, an array's in its order, none for an absent field.
|
|
172
|
+
*/
|
|
173
|
+
const liftedFrom = (lift, success) => lift.fields.flatMap(({ name, kind, many }) => {
|
|
174
|
+
const value = name === void 0 ? success : Predicate.hasProperty(success, name) ? success[name] : void 0;
|
|
175
|
+
return (many ? isMediaArray(value) ? value : [] : isMedia(value) ? [value] : []).map((media) => ({
|
|
176
|
+
kind,
|
|
177
|
+
field: name,
|
|
178
|
+
value: media
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
51
181
|
//#endregion
|
|
52
|
-
export { defaultPath, httpProtocol, statelessRequest };
|
|
182
|
+
export { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, statelessRequest };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Schema, SchemaAST } from "effect";
|
|
2
|
+
//#region src/internal/media.d.ts
|
|
3
|
+
/** An image, a media field's schema, as `Action.Image` exports it. */
|
|
4
|
+
export declare const Image: Schema.Struct<{
|
|
5
|
+
readonly data: Schema.Uint8Array;
|
|
6
|
+
readonly mimeType: Schema.String;
|
|
7
|
+
}>;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/internal/media.ts
|
|
3
|
+
/**
|
|
4
|
+
* The annotation marking a media kind's schema, naming the MCP content block a value of it is
|
|
5
|
+
* lifted into: `Action.Image`'s is `"image"`.
|
|
6
|
+
*/
|
|
7
|
+
const mediaKey = "effect-actions/media";
|
|
8
|
+
/** What every media kind holds: bytes, and their MIME type. */
|
|
9
|
+
const Media = Schema.Struct({
|
|
10
|
+
data: Schema.Uint8Array,
|
|
11
|
+
mimeType: Schema.String
|
|
12
|
+
});
|
|
13
|
+
/** An image, a media field's schema, as `Action.Image` exports it. */
|
|
14
|
+
const Image = Media.annotate({ [mediaKey]: "image" });
|
|
15
|
+
/**
|
|
16
|
+
* Whether `ast` is marked as media, read from its own annotations: a copy, such as an optional
|
|
17
|
+
* key or one with checks added, keeps them, and so does one with an encoding of its own.
|
|
18
|
+
*/
|
|
19
|
+
const isMarked = (ast) => ast.annotations?.[mediaKey] === "image";
|
|
20
|
+
/**
|
|
21
|
+
* The kind `ast` is, when its values are the media they encode to: marked, without an encoding
|
|
22
|
+
* of its own, which would send its values as something else.
|
|
23
|
+
*/
|
|
24
|
+
const kindOf = (ast) => isMarked(ast) && ast.encoding === void 0 ? "image" : void 0;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { Image, Media, isMarked, kindOf };
|
|
@@ -10,11 +10,11 @@ import { HttpApiMiddleware } from "effect/http-api";
|
|
|
10
10
|
const responseKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
|
|
11
11
|
/**
|
|
12
12
|
* The native middleware answering schema failures: a request that does not decode with
|
|
13
|
-
* `InvalidInput`, the schema's own message and issues
|
|
14
|
-
*
|
|
13
|
+
* `InvalidInput`, the schema's own message and issues. A result that does not encode stays the
|
|
14
|
+
* native failure, which Effect answers with an empty 500 and reports.
|
|
15
15
|
*/
|
|
16
16
|
var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
|
|
17
17
|
/** `SchemaErrors`' server side, built by `ActionHttp.layer` alone, so a client's bundle leaves it out. */
|
|
18
|
-
const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => responseKinds.has(failure.kind) ?
|
|
18
|
+
const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(responseKinds.has(failure.kind) ? failure : InvalidInput.fromSchemaError(failure.cause)));
|
|
19
19
|
//#endregion
|
|
20
20
|
export { SchemaErrors, schemaErrors };
|
package/docs/Action.md
CHANGED
|
@@ -13,31 +13,32 @@ surface declares and any handler may fail with.
|
|
|
13
13
|
|
|
14
14
|
Import `@gjermundgaraba/effect-actions/Action`.
|
|
15
15
|
|
|
16
|
-
| Export | Purpose
|
|
17
|
-
| ----------------------------------------------- |
|
|
18
|
-
| `make(name, options)` | Define a pure contract; literal names, `readOnly` and `caller` stay typed.
|
|
19
|
-
| `byName(actions)` | A list of actions keyed by name, each its exact contract: `contracts.getUser`.
|
|
20
|
-
| `implement(action, handler, { authorize })` | Bind one handler, and a protected action's authorizer; returns one `Implementation`.
|
|
21
|
-
| `implement([actions], handlers, { authorize })` | Bind a record of handlers keyed by action name; returns one `Implementation` of all.
|
|
22
|
-
| `implement(actions, builder, options?)` | Either form, with an Effect that builds the handler or record once per layer graph.
|
|
23
|
-
| `allowAll` | The authorizer without an action-level rule: every authenticated caller may call.
|
|
24
|
-
| `Anyone` | The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not.
|
|
25
|
-
| `layer(implementations)` | Their builders as one layer: provided above every surface, each runs once for all.
|
|
26
|
-
| `client(implementations, { actions? })` | An Effect of a caller running them in process: `client.<action>(input)`.
|
|
27
|
-
| `
|
|
28
|
-
| `InvalidInput
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
16
|
+
| Export | Purpose |
|
|
17
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `make(name, options)` | Define a pure contract; literal names, `readOnly` and `caller` stay typed. |
|
|
19
|
+
| `byName(actions)` | A list of actions keyed by name, each its exact contract: `contracts.getUser`. |
|
|
20
|
+
| `implement(action, handler, { authorize })` | Bind one handler, and a protected action's authorizer; returns one `Implementation`. |
|
|
21
|
+
| `implement([actions], handlers, { authorize })` | Bind a record of handlers keyed by action name; returns one `Implementation` of all. |
|
|
22
|
+
| `implement(actions, builder, options?)` | Either form, with an Effect that builds the handler or record once per layer graph. |
|
|
23
|
+
| `allowAll` | The authorizer without an action-level rule: every authenticated caller may call. |
|
|
24
|
+
| `Anyone` | The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not. |
|
|
25
|
+
| `layer(implementations)` | Their builders as one layer: provided above every surface, each runs once for all. |
|
|
26
|
+
| `client(implementations, { actions? })` | An Effect of a caller running them in process: `client.<action>(input)`. |
|
|
27
|
+
| `Image` | A media field's schema, `{ data: Uint8Array, mimeType: string }`: an image, which MCP sends as an image block. |
|
|
28
|
+
| `InvalidInput`, `Unauthenticated`, `Forbidden` | Built-in errors: 400, 401, 403, body `{ _tag, message }`, `InvalidInput`'s with `issues`; `message` defaults. |
|
|
29
|
+
| `InvalidInput.fromSchemaError(error)` | The `InvalidInput` every surface answers a `Schema.SchemaError` with, `issues` given. |
|
|
30
|
+
| `Issue` | One of `InvalidInput`'s `issues`: a `path` into the input and a `message`. |
|
|
31
|
+
| `Refusal` | `Unauthenticated \| Forbidden`, schema and type: what authentication or an authorizer refuses with. |
|
|
32
|
+
| `BuiltIn` | `InvalidInput \| Refusal`, schema and type: what any handler may fail with. |
|
|
33
|
+
| `Errors` | A list of error schemas, as `error` holds it: `<E extends Action.Errors>`. |
|
|
34
|
+
| `Action`, `Any`, `Implementation` | Concrete and erased contracts, and bound implementations. |
|
|
35
|
+
| `Client<Apps>` | What `client` gives for the implementations `Apps`, one or a list. |
|
|
36
|
+
| `AnyImplementation`, `AnyImplementation<A>` | Any implementation, or any of actions `A`, erased: a generic helper's constraint. |
|
|
37
|
+
| `BuildServices`, `BuildError` | What their builders read and fail with at startup, which a surface serving them owes. |
|
|
38
|
+
| `implementation.actions` | Its exact contract values, as `Testing.mcpClient(users.actions)` takes them. |
|
|
39
|
+
| `Handler`, `Authorize` | Typed handlers, and authorizers `(action) => Effect<void, Refusal, R>`. |
|
|
40
|
+
| `Handlers` | A list's handlers keyed by name, for a builder written apart from `implement`. |
|
|
41
|
+
| `Options`, `ClientOptions`, `Mcp` | What `make` and `client` take, and `make`'s `mcp` options. |
|
|
41
42
|
|
|
42
43
|
| Option | Meaning |
|
|
43
44
|
| ------------- | ---------------------------------------------------------------------------------------------- |
|
|
@@ -372,6 +373,7 @@ console.log(
|
|
|
372
373
|
- An `undefined` option takes its default, as an omitted one does. One that may be either is typed as either: with `success: enabled ? Schema.String : undefined`, or the same through a conditional spread, the success is `string | void`. The same holds for `input` and `error`.
|
|
373
374
|
- Options annotated with a wide type, such as `Parameters<typeof Action.make>[1]`, are not checked. Their action's schemas are as wide as what may run, so its success is `unknown`. A constant object spread into the options, as above, keeps its exact types and is checked like inline options.
|
|
374
375
|
- MCP input must be one object with keys, an identified, recursive or suspended root included. Scalar, array or union input is fine for HTTP and for a native Toolkit, but `ActionMcp` refuses it when `layerHttp` or `runStdio` is called, naming the action ([ActionMcp.md](ActionMcp.md#rules)). Success and error schemas may be any shape.
|
|
376
|
+
- `Action.Image` is a media field, an image: `{ data: Uint8Array, mimeType: string }`, `mimeType` such as `image/png`. It goes in the success, as a top-level field of a struct, `success: { screen: Screen, image: Action.Image }`, optional, `Schema.optional(Action.Image)`, or an array, `Schema.Array(Action.Image)` or `Schema.NonEmptyArray(Action.Image)`, or as the whole success, `success: Action.Image`, or an array of it. An MCP tool lifts it into an image block, the content a model reads as an image; every other surface sends it as JSON, the bytes in base64 ([guarantees.md](guarantees.md#wire-behavior)). Anywhere else, nested in a field, in a union or a record, in an optional array field, in a class or an encoded struct, given an encoding of its own, in the input or in an error, the other surfaces still send it as JSON, but `ActionMcp` refuses it when `layerHttp` or `runStdio` is called, naming the actions ([ActionMcp.md](ActionMcp.md#media)): leave such an action out of its `actions`, or move the image to a top-level field.
|
|
375
377
|
- The contract keeps `mcp` as given, `action.mcp`, and defaults none of it. A hint left out is not annotated, so the native default, MCP's, stands: `destructiveHint` `true`, `idempotentHint` `false`, `openWorldHint` `true`; but a read-only action's `destructiveHint` is `false` unless it states one. A stated hint is sent as it is, a read-only action's included, which MCP reads only for a tool that is not read-only. `readOnlyHint` is always `readOnly`. Hints are metadata for the model. They do not enforce authorization, approval, or retries; a native Toolkit's approval is `ActionToolkit.make`'s `needsApproval` option.
|
|
376
378
|
- The built-in errors are declared everywhere ([guarantees.md](guarantees.md#wire-behavior)), and any handler may fail with them without listing them: `InvalidInput` for input that decodes but cannot be served, naming `issues` of its own as the library's do ([guarantees.md](guarantees.md#wire-behavior)), `new Action.InvalidInput({ message, issues: [{ path: ["lines"], message }] })`, so an application declares no second error for bad input; a refusal for a step-up. `make` refuses an `error` entry that encodes with a built-in tag, the built-in itself included, since every surface declares it already and a client could not tell a look-alike apart. Only those classes are built-in errors: a handler or an authorizer failing with an error of the same `_tag` and fields, such as Effect's `HttpApiError.Forbidden` or a `Forbidden` of your own, does not compile, since a built-in error's codec encodes its own class alone.
|
|
377
379
|
- Code that decodes input of its own, such as a header, a route parameter or a file's metadata, answers a failure as the surfaces do with `Action.InvalidInput.fromSchemaError(error)`: the schema's message and its `issues`, `Effect.mapError(Action.InvalidInput.fromSchemaError)` after a `Schema.decodeUnknownEffect`. To place the issues under a field of a larger input, decode the value as that field, `Schema.decodeUnknownEffect(Schema.Struct({ payload: Payload }))({ payload })`, so each issue's `path` starts at it.
|
package/docs/ActionCli.md
CHANGED
|
@@ -261,6 +261,7 @@ export const cli = ActionCli.make(cache, { name: "ops" }).pipe(
|
|
|
261
261
|
- The flags follow the schema: renaming a field renames its flag. `name`, `positional`, `aliases` and `render` give a command its name, its arguments, its short flags and its output, and keep every rule of this page: prefer a derived command wherever they express the syntax. For a syntax they cannot, such as a flag named apart from its field, or a command calling several actions, build the command with native `Command.make`, `Flag` and `Argument`, and call the actions in it through `Action.client`, acquired in its handler, inside `Effect.scoped`, so each invocation builds and releases it ([Action.md](Action.md#clients)), or over HTTP through the binding's `ActionHttp.client`. Calling a handler directly bypasses decoding and the implementation's authorizer.
|
|
262
262
|
- Such a command is the host's own: through `Action.client` the authorizer runs, but the output, failure and logging rules of this page do not apply. It prints what it prints, its logs go where the host's logger writes, and a failure it leaves as it is is the action's own, not a `UserError`, which `runMain` reports on stdout with a stack and without its fields. Map it to Effect CLI's `CliError.UserError`, `new CliError.UserError({ cause, userMessage })`, for `Command.run` to print `userMessage` on stderr.
|
|
263
263
|
- Output is validated and encoded before printing. Default output is JSON. `render(decoded)` gives human output and adds a `--json` flag to that command, which selects JSON again. An action whose `success` is `Schema.Void` prints nothing by default or with `--json`; a custom `render` can still print human output. Rendering cannot bypass validation: a success its schema does not encode is a defect, as on HTTP, and nothing prints it.
|
|
264
|
+
- A media field, `Action.Image`, is printed as JSON, by default and with `--json`: `{ "data": "<base64>", "mimeType": "image/png" }`, as HTTP sends it. A command's `render` receives the decoded success, the image's `data` a `Uint8Array`, and prints the text it returns instead, such as the image's size; it returns text, never bytes.
|
|
264
265
|
- `--json` is a regular flag of the rendered command only. A command without `render` has no such flag: its output is JSON already. Nothing is declared tree-wide, so a host CLI may declare its own `--json`, global or not.
|
|
265
266
|
- Each invocation builds the selected implementation's builder, and its authorizer when an Effect builds it, in a scope of its own and releases them after the call. Only that implementation's builder runs: other implementations passed alongside it, on `command` or `make`, are not built. Domain services and authority come from layers the host provides on the command, `Command.provide(layer)`: built when the command runs, before its input is decoded, so input the action's schema refuses is refused after they are built; `--help` and the parser's errors, such as a missing or unknown flag, never build them. `make`'s aggregate run alone builds them before showing its help, since its own handler shows it, and fails instead if one fails. There is no HTTP fallback.
|
|
266
267
|
- A local command is a local surface: the implementation's `authorize` runs before a protected action's handler, on `command` and on every subcommand of `make`, and the host provides the identity a protected action declares on the command, `Command.provideSync(CurrentActor, actor)`, or `Command.provideEffect(CurrentActor, load)` to read it when an action runs. Its type requires it whether or not the handler or the authorizer reads it.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -229,6 +229,7 @@ In a browser, relative routes resolve against the page; elsewhere, give `baseUrl
|
|
|
229
229
|
- Bindings combine into one host API with the native `addHttpApi` method, `HttpApi.make("app").addHttpApi(Http.api)`, for one document or one native client, only when their prefixes differ and no action name repeats across them. An operation ID is the action name, and a group is keyed by its mount path. Serving several bindings with `layer` has neither limit.
|
|
230
230
|
- Serve the OpenAPI document as a native route: `HttpRouter.add("GET", "/api/openapi.json", HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)))`. It documents every bound action, not only the served ones. It is a plain route: middleware provided to its layer covers it, and nothing covers it otherwise.
|
|
231
231
|
- A route runs only a request typed as JSON: any other type, or none, is a 415, after authentication on a protected route, as on an MCP endpoint, whatever encoding the input is annotated with, such as `HttpApiSchema.asFormUrlEncoded()`, which `HttpApi` would read and no other surface does. So no request a page on another origin can send without a CORS preflight reaches a handler, whatever credentials, cookies included, it carries; one typed as JSON is preflighted, and the host's CORS policy decides.
|
|
232
|
+
- A media field, `Action.Image`, is JSON in the body: `{ "data": "<base64>", "mimeType": "image/png" }`, as Effect's JSON codec encodes a `Uint8Array`. The OpenAPI document lists `data` as `type: string`, `format: byte`, `contentEncoding: base64`, as Effect's JSON Schema does, and `client` decodes it to a `Uint8Array`. Only MCP lifts it into a block of its own ([ActionMcp.md](ActionMcp.md#media)).
|
|
232
233
|
- Wire format: [guarantees.md](guarantees.md#wire-behavior). Spans and log annotations: [guarantees.md](guarantees.md#observability).
|
|
233
234
|
|
|
234
235
|
### Binding errors
|
package/docs/ActionMcp.md
CHANGED
|
@@ -136,6 +136,83 @@ ActionMcp.runStdio(status, { name: "effect-actions-stdio", version: "0.1.0" }).p
|
|
|
136
136
|
);
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
### Media
|
|
140
|
+
|
|
141
|
+
A tool returning an image is an action like the others: an `Action.Image` field of its success
|
|
142
|
+
([Action.md](Action.md#contracts)) is lifted into an image block, the content a model reads as
|
|
143
|
+
an image, and the rest of the success is sent as any success is.
|
|
144
|
+
|
|
145
|
+
```ts example=mcp-image.ts
|
|
146
|
+
import { Buffer } from "node:buffer";
|
|
147
|
+
import { NodeRuntime, NodeStdio } from "@effect/platform-node";
|
|
148
|
+
import { Effect, Schema } from "effect";
|
|
149
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
150
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
151
|
+
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
152
|
+
|
|
153
|
+
const Screen = Schema.Struct({ id: Schema.Int, title: Schema.String });
|
|
154
|
+
|
|
155
|
+
class TerminusError extends Schema.TaggedError<TerminusError>()("TerminusError", {
|
|
156
|
+
message: Schema.String,
|
|
157
|
+
}) {}
|
|
158
|
+
|
|
159
|
+
// The image is a field of the success. Over MCP it is lifted into an image block, after the
|
|
160
|
+
// JSON text of `{ screen }`, which is also the structured content; elsewhere it is JSON, the
|
|
161
|
+
// bytes in base64.
|
|
162
|
+
const GetScreenImage = Action.make("get_screen_image", {
|
|
163
|
+
description: "Fetch the rendered image for a listed screen.",
|
|
164
|
+
input: { screen_id: Schema.Int },
|
|
165
|
+
success: { screen: Screen, image: Action.Image },
|
|
166
|
+
error: TerminusError,
|
|
167
|
+
readOnly: true,
|
|
168
|
+
caller: Action.Anyone,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// A one-pixel PNG stands in for a rendered screen.
|
|
172
|
+
const pixel = Buffer.from(
|
|
173
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
174
|
+
"base64",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const getScreenImage = Action.implement(GetScreenImage, ({ screen_id }) =>
|
|
178
|
+
screen_id === 1
|
|
179
|
+
? Effect.succeed({
|
|
180
|
+
screen: { id: screen_id, title: "Home" },
|
|
181
|
+
image: { data: pixel, mimeType: "image/png" },
|
|
182
|
+
})
|
|
183
|
+
: Effect.fail(new TerminusError({ message: `No screen ${screen_id}.` })),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// A subprocess MCP server, as mcp-stdio.ts is: launch it from an MCP client.
|
|
187
|
+
ActionMcp.runStdio(getScreenImage, { name: "terminus", version: "0.1.0" }).pipe(
|
|
188
|
+
Effect.provide(NodeStdio.layer),
|
|
189
|
+
ActionCli.logToStderr,
|
|
190
|
+
NodeRuntime.runMain,
|
|
191
|
+
);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`get_screen_image` with `{ "screen_id": 1 }` answers, on every revision, beside the fields the
|
|
195
|
+
native server adds to every result ([Rules](#rules)):
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"content": [
|
|
200
|
+
{ "type": "text", "text": "{\"screen\":{\"id\":1,\"title\":\"Home\"}}" },
|
|
201
|
+
{
|
|
202
|
+
"type": "image",
|
|
203
|
+
"data": "iVBORw0KGgo...",
|
|
204
|
+
"mimeType": "image/png",
|
|
205
|
+
"_meta": { "effect-actions/field": "image" }
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"structuredContent": { "screen": { "id": 1, "title": "Home" } }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The tool lists the `outputSchema` of `{ screen }`. HTTP, a CLI command and a Toolkit send the
|
|
213
|
+
same success as JSON, the image `{ "data": "<base64>", "mimeType": "image/png" }`
|
|
214
|
+
([guarantees.md](guarantees.md#wire-behavior)).
|
|
215
|
+
|
|
139
216
|
### Native features
|
|
140
217
|
|
|
141
218
|
Effect's own resources, prompts and tools join an endpoint's tools as its `features`, one layer
|
|
@@ -181,14 +258,21 @@ satisfy a feature.
|
|
|
181
258
|
- A handler may yield `McpServer.McpServer`, its endpoint's registry, to send notifications such as progress: the endpoint provides it, and no host owes it. Served on another surface too, where nothing provides it, read it with `Effect.serviceOption`.
|
|
182
259
|
- Every served action's input must be one object with keys, as a tool's arguments are: fields, a struct or a class, identified, recursive or suspended, a record, or a declared type whose JSON Schema is an object. `layerHttp` and `runStdio` read the JSON Schema the native server reads, and throw for any other when they are called, naming the actions: a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Omit `input`, or give `{}`, for a tool with no arguments.
|
|
183
260
|
- A tool's `inputSchema` is its input's JSON Schema, on every revision, closed with `additionalProperties: false` where the input declares its fields; a record's lists its value schema there instead. Undeclared arguments are refused on every revision, as invalid arguments.
|
|
184
|
-
- A success is sent as it is. On 2026-07-28, over HTTP and stdio, it is `structuredContent: <encoded success>`, of any JSON type (`null` for an action that returns nothing), and one text block holding the same JSON, and the tool's `outputSchema` describes the encoded success. Stdio's earlier revisions, 2025-11-25 and 2025-06-18, structure less: they carry only an object as `structuredContent`, and list only an object-rooted `outputSchema`. A success they do not structure is text alone: its JSON, or a string success the string itself. A declared error is an `isError` result whose text content is the error's JSON encoding, the same bytes HTTP sends as the body, and no `structuredContent`.
|
|
261
|
+
- A success without a media field is sent as it is. On 2026-07-28, over HTTP and stdio, it is `structuredContent: <encoded success>`, of any JSON type (`null` for an action that returns nothing), and one text block holding the same JSON, and the tool's `outputSchema` describes the encoded success. Stdio's earlier revisions, 2025-11-25 and 2025-06-18, structure less: they carry only an object as `structuredContent`, and list only an object-rooted `outputSchema`. A success they do not structure is text alone: its JSON, or a string success the string itself. A declared error is an `isError` result whose text content is the error's JSON encoding, the same bytes HTTP sends as the body, and no `structuredContent`.
|
|
262
|
+
- A media field is lifted out of the result ([Media](#media)): each becomes a content block of its own, after the one text block, in field order: for an `Action.Image`, an `image` block of its `data` in base64 and its `mimeType`. The rest of the success is sent as any success is: `structuredContent` is the encoded success without its media fields, the tool's `outputSchema` describes that, and the text block is its JSON, exactly. The rest is an object, so stdio's 2025 revisions structure it too.
|
|
263
|
+
- An absent optional media field sends no block. An array field sends one block per element, in order.
|
|
264
|
+
- A success that is media, `success: Action.Image`, or whose every field is, sends its blocks alone: no `structuredContent`, no text block, and the tool lists no `outputSchema`. A success that is an array of media, `success: Schema.Array(Action.Image)`, sends one block per element, alone too. Blocks of a success that is media carry no `_meta`, as no field names them.
|
|
265
|
+
- Each block lifted from a field names it in its `_meta`, `{ "effect-actions/field": "image" }`, on every revision, so a client reassembles the success from the structured content and the blocks, as `Testing.mcpClient` does ([Testing.md](Testing.md#rules)). A client that does not know the key ignores it, as MCP's `_meta` is open.
|
|
266
|
+
- Media is lifted only from a top-level field of a struct success (`Schema.Struct` or fields, `Schema.suspend`ed or not), the field bare, `Schema.optional` or `Schema.optionalKey`, or an array, `Schema.Array` or `Schema.NonEmptyArray`, or from a success that is media or such an array. An `Action.Image` anywhere else in an action the endpoint serves is refused when `layerHttp` or `runStdio` is called, naming the actions, as an input that is not one object is: nested in a field, in a union or a record, in an optional array field, whose blocks cannot tell absent from empty, in a `Schema.Class` success or a struct with an encoding of its own, such as `Schema.encodeKeys`, or given an encoding of its own itself, which would send it as something else, in the input or in an error. Only the listed `actions` are checked.
|
|
267
|
+
- A media field's own annotations, such as its `description`, and those of a success that is media, reach no model: the `outputSchema` leaves the field out, a success that is media lists none, and an image block carries no description. Describe the image in the action's `description`.
|
|
268
|
+
- A declared error is unchanged by media: an `isError` result whose text is the error's JSON.
|
|
185
269
|
- On 2026-07-28, over HTTP and over stdio, the native server adds `_meta["io.modelcontextprotocol/serverInfo"]` and `resultType: "complete"` to every result, beside a tool result's own fields (`isError: false` on a success); the earlier revisions stdio speaks add neither. `serverInfo` is the options' `name`, `version`, `description`, `websiteUrl` and `icons`, as given, so a 2026-07-28 result's encoded size is the size of its own fields plus a fixed overhead per endpoint or subprocess.
|
|
186
270
|
- Invalid arguments are answered by the native `McpServer`: from 2025-11-25 on, an `isError` result with a message for the model, such as `Invalid parameters for tool 'greet': Expected string\n at ["name"]`; on earlier stdio revisions, a JSON-RPC error. HTTP's `InvalidInput` does not apply. A protected tool's call from a caller without a credential that verifies gets the 401 first, before its arguments are decoded, malformed ones included, on a mixed endpoint as on one of protected tools alone.
|
|
187
271
|
- Defects and encoding failures produce the generic `isError` text `Tool execution failed due to an internal server error.`; the cause is logged, not sent.
|
|
188
272
|
- Every caller sees every tool of the endpoint ([guarantees.md](guarantees.md#scope)); authorization happens in each implementation's `authorize`.
|
|
189
273
|
- Cancellation is Effect's native RPC interruption. Over HTTP there is no session, so `notifications/cancelled` interrupts nothing. A tool call ends with its HTTP request, whose lifetime (for example, whether a client disconnect interrupts it) is the host's.
|
|
190
274
|
- Handlers may yield `McpSchema.McpRequestContext` for the client's declared information; the native server supplies it to every tool call, so it is never a router or host requirement.
|
|
191
|
-
- `runStdio` is the subprocess's whole program: it serves until the host closes stdin, then succeeds, so the process exits 0. Closing stdin
|
|
275
|
+
- `runStdio` is the subprocess's whole program: it serves until the host closes stdin, then succeeds, so the process exits 0. Closing stdin ends the input, not the calls in flight: each runs to completion and is answered, and `runStdio` succeeds once every answer is written. A script piping requests may close stdin as soon as it has written them. A call that never completes keeps the process running: MCP hosts close stdin to shut a server down, and the MCP specification expects them to send `SIGTERM` when it does not exit in time. A signal interrupts it, as any program. Provide `Stdio` and its services to it and run it, `NodeRuntime.runMain`.
|
|
192
276
|
- stdio is a local surface and takes no `authentication`: the host supplies `Stdio` (`NodeStdio.layer`) and any request-time services explicitly around `runStdio`, the identity of every protected tool included, `Effect.provideService(CurrentActor, actor)`. Each implementation's `authorize` runs for its protected tools. Tool arguments never establish identity.
|
|
193
277
|
- `runStdio` gives its program a `Console` that writes every method through the `error` of the console it runs with, to stderr, since stdout carries the protocol: every console logger, the default one included, `Console.log`, and the counters, timers and group labels Node's console prints on stdout. A counter or a timer prints its label and its count or the milliseconds since it started, a group prints its label without indenting what follows, and `clear` does nothing. Layers provided around `runStdio` run outside its program: apply `ActionCli.logToStderr` last, before `runMain`, as the example does, which moves their default logger to stderr and reports there what `runMain` would report on stdout, a failure of such a layer or a defect, with the exit code `runMain` gives ([ActionCli.md](ActionCli.md#rules)). It does not move their `Console` output or a logger writing through `Console.log`, such as `Logger.consoleJson`; log JSON with `Logger.withConsoleError(Logger.formatJson)` instead. Keep the global `console.log` and other direct writes off stdout.
|
|
194
278
|
- Each endpoint or subprocess owns a fresh native tool registry. That isolates tool names, not application context.
|
|
@@ -201,6 +285,7 @@ satisfy a feature.
|
|
|
201
285
|
## Failure modes
|
|
202
286
|
|
|
203
287
|
- `MCP tool input must be one object with keys, such as a struct: <name>, ...` thrown by `layerHttp` or `runStdio`: those actions' input is a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Wrap a union in a field, `input: { notification: Schema.Union([Email, Sms]) }`, omit `input` (or give `{}`) for no arguments, or leave the action out of `actions`.
|
|
288
|
+
- `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: <name>, ...` thrown by `layerHttp` or `runStdio`: those actions hold an `Action.Image` a tool cannot lift, nested in a field, in a union or a record, in an optional array field, in a class or an encoded struct, with an encoding of its own, in the input or in an error. Make it a top-level field of a `Schema.Struct` success, the whole success or an array of it, give an optional array field as a required one, empty when absent, or leave the action out of `actions`; the other surfaces send it as JSON.
|
|
204
289
|
- `Type 'CurrentActor' is not assignable to type 'never'` where the server is launched, with `CurrentActor` among the endpoint's startup requirements rather than its `Request<"Requires", ...>`: a feature reads a request service, which it never receives ([native features](#native-features)). Serve that content as an action.
|
|
205
290
|
- A native `McpServer.resource`, `McpServer.prompt` or `McpServer.toolkit` layer merged beside `layerHttp` builds without error and is never served: `resources/list` is empty, `prompts/list` is not found, and `tools/list` lists only the actions. Each endpoint's registry is its own: pass them as its `features`.
|
|
206
291
|
- An MCP client gets an `isError` refusal instead of the 401 or 403 it re-authorizes on: the handler sent a notification before refusing, so the response had already started. Refuse in the implementation's `authorize`, before the handler runs.
|
package/docs/ActionToolkit.md
CHANGED
|
@@ -105,6 +105,7 @@ provide it once. So does one of fewer tools, such as an agent's
|
|
|
105
105
|
|
|
106
106
|
- Every action of the implementations passed becomes a tool, named after the action, with its `mcp` hints, unless `actions` lists the tools: then only those, each behind its implementation's authorizer and from its builder's one run, and an implementation holding none of them is not built. `make(implementations)` accepts one implementation or a list, such as `[userActions, double]`. The types take only actions of the implementations, and type `toolkit` and `layer`'s build services by the listed actions alone; `needsApproval`'s `call` is typed by every action of the implementations. Options whose `actions` may be absent, such as a value typed `ActionToolkit.Options<A>`, serve every action when it is absent and the listed ones when given, so their types owe what every action owes, and hold only the tools present either way ([guarantees.md](guarantees.md#names)).
|
|
107
107
|
- A result's `result` is the action's decoded success or declared failure, and its `encodedResult` the JSON a model reads, such as `"42"` for a `BigInt`. Declared failures are returned as native tool results (`failureMode: "return"`), not raised.
|
|
108
|
+
- A media field, `Action.Image`, reaches a model as base64 text: the tool's `encodedResult` is the JSON success, `{ "data": "<base64>", "mimeType": "image/png" }` for an image, as HTTP sends it, and Effect's providers send a tool's result to the model as text. The model reads no image from it, and the base64 fills its context. Leave such an action out of `actions` unless the model needs those bytes; over MCP the same action sends an image block ([ActionMcp.md](ActionMcp.md#media)).
|
|
108
109
|
- `tools.handle(name, encodedInput)` takes JSON arguments, as a model sends them: an ISO string for a `Schema.Date`, `null` for an absent `Schema.optional` field. It returns an Effect producing a result stream. The handler starts while that stream is constructed, so request services must be provided around the entire `handle(...).pipe(Effect.flatMap(Stream.runCollect))`, not only around the stream.
|
|
109
110
|
- Builders, calls and identity follow the [dependency lifetimes](guarantees.md#dependency-lifetimes), and authorization the [authorization rules](guarantees.md#authorization): `layer` builds, a call releases what it acquired when it ends, whether a model's turn or `tools.handle` makes it, and identity is supplied at invocation, never when building the layer. A protected tool's call owes its identity in its type; one call without it anyway, as from plain JavaScript, fails with `Unauthenticated` before `authorize` runs. The innermost caller provided wins: a call made under a narrower identity runs as that one.
|
|
110
111
|
- A model loop in a route yields `toolkit` in its implementation's builder, `Action.implement(Chat, Effect.map(toolkit, (tools) => ({ prompt }) => ...), { authorize })`, and passes `tools` to `LanguageModel.generateText`; the route's layer takes the toolkit's `layer`, `ActionHttp.layer(ChatHttp, chat).pipe(Layer.provide(layer))`. That is one build, in the routes' layer graph, shared with every surface serving the same implementations. `Effect.provide(layer)` in the handler builds it for every call unless a surface of the graph serves the same implementations, and `HttpRouter.provideRequest(layer)` once, but in a layer graph of its own: a second build when another surface serves them and has not built them first.
|
|
@@ -119,6 +120,7 @@ provide it once. So does one of fewer tools, such as an agent's
|
|
|
119
120
|
|
|
120
121
|
## Failure modes
|
|
121
122
|
|
|
123
|
+
- A model reads base64 text where an image was meant: a tool's success holds an `Action.Image`, which a Toolkit gives the model as JSON text. Leave the action out of `actions`, or serve it over MCP, which sends an image block.
|
|
122
124
|
- Tool missing from the toolkit: its implementation was not passed to `make`, or `actions` leaves it out.
|
|
123
125
|
- `Listed in actions, but no implementation holds it: <names>` thrown at `make`: those actions are not among the implementations' actions, by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding it, or list the contract it implements.
|
|
124
126
|
- A defect when a tool handles a call: no `layer` of its implementation is provided, only one of another implementation with tools of the same names, such as another implementation of the same actions, which the types accept. Provide a `layer` made from the implementations the `toolkit` was made from.
|
package/docs/CONTEXT.md
CHANGED
|
@@ -26,3 +26,4 @@ words with these meanings. Module names and Effect's own terms (`HttpApi`, `Tool
|
|
|
26
26
|
- **Declared error**: an error schema listed in an action's `error`. Its handler may fail with it, a limit such as a rate limit included. HTTP sends its JSON encoding with its `httpApiStatus`, 422 without one; MCP returns the same JSON as an `isError` result; the Toolkit returns it as a failure result; a CLI command prints the same JSON on stderr, failing with Effect CLI's `UserError`, whose `cause` it is.
|
|
27
27
|
- **Built-in errors**: `Action.InvalidInput` (400), `Action.Unauthenticated` (401) and `Action.Forbidden` (403), tagged errors with body `{ _tag, message }` and a default `message`; an `InvalidInput` also lists `issues`, empty when it names none, each an `Action.Issue`: a `path` into the input and a `message`, and a `Forbidden` the OAuth `scopes` a call lacks. The library produces them, and every surface declares them: [guarantees.md](guarantees.md#wire-behavior).
|
|
28
28
|
- **Refusal**: `Action.Refusal`, `Unauthenticated | Forbidden`, a schema and a type, as `Action.BuiltIn` is of every built-in error. What authentication or an authorizer refuses a caller with instead of letting a handler run, and all an authorizer may fail with. A **step-up refusal** is one an OAuth client acts on, `Unauthenticated` or a `Forbidden` naming `scopes`. Its HTTP status, challenge, and the exception for an MCP response that has already started follow the [authorization rules](guarantees.md#authorization).
|
|
29
|
+
- **Media field**: a field of an action's success whose schema is a media kind, `Action.Image`, `{ data: Uint8Array, mimeType: string }`: a top-level field of a struct success, optional or an array, or the whole success or an array of it. An MCP tool **lifts** it out of its result into a content block of its own, an image block, beside the rest of the success; every other surface sends it as JSON, the bytes in base64 ([ActionMcp.md](ActionMcp.md#media)).
|
package/docs/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Read the card for a module before writing code that uses it. Exported TypeScript
|
|
|
10
10
|
Package facts that apply everywhere:
|
|
11
11
|
|
|
12
12
|
- Every module is a subpath import: `import * as Action from "@gjermundgaraba/effect-actions/Action"`. There is no package root.
|
|
13
|
-
- The `effect` peer
|
|
13
|
+
- The `effect` peer admits the patches of the Effect release the package is tested against, and no later minor until a release is tested against it ([setup.md](setup.md#install)).
|
|
14
14
|
- Action contracts are pure values. Defining contracts and implementations runs neither handlers nor builder Effects. Builder services are acquired when a layer is built, an `Action.client` is acquired, or a local CLI command runs; see [dependency lifetimes](guarantees.md#dependency-lifetimes).
|
|
15
15
|
|
|
16
16
|
## Pages
|
package/docs/Testing.md
CHANGED
|
@@ -144,9 +144,9 @@ Any provider of that descriptor satisfies the routes; nothing else does.
|
|
|
144
144
|
- A relative URL resolves against `http://localhost`, once any `baseUrl` a client adds is applied, so `ActionHttp.client(Http)` needs no `baseUrl` under `layer`, and one given is kept. Every request on this client is answered by the routes, whatever its host, so the native `HttpApiClient` and a remote `ActionCli` command work in memory too.
|
|
145
145
|
- The client is `layer`'s own: the program's other HTTP clients get none of its requests, and it none of theirs, whether a `FetchHttpClient` is built before or after it or a `FetchHttpClient.Fetch` is provided around the program.
|
|
146
146
|
- `layer(handler)` gives the same client, answered by a web handler the test serves, such as `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler` shared with tests that send it `Request`s: the routes are built once, by the handler, for every `layer` given it. The web handler needs the routes' platform services, which `HttpServer.layerServices` provides and `layer(routes)` supplies itself. The test owns the handler: `layer` neither builds nor disposes it, so the test calls `dispose` when it is done.
|
|
147
|
-
- A client method sends one stateless `tools/call` for the action's tool, at the protocol version `ActionMcp.layerHttp` serves, 2026-07-28, in both the header and `_meta`. It encodes the input with the action's schema, and decodes the success from `structuredContent`, as `ActionHttp.client` does for a route. It reads a JSON or an event-stream response.
|
|
147
|
+
- A client method sends one stateless `tools/call` for the action's tool, at the protocol version `ActionMcp.layerHttp` serves, 2026-07-28, in both the header and `_meta`. It encodes the input with the action's schema, and decodes the success from `structuredContent`, as `ActionHttp.client` does for a route. A media field it takes only from the blocks whose `_meta` names it, `{ "effect-actions/field": "image" }`, in order, never from the structured content, and a success that is media, or whose every field is, from its blocks alone ([ActionMcp.md](ActionMcp.md#media)): the method succeeds with the success as the handler returned it, the image's `data` a `Uint8Array`, an optional image returned as `undefined` absent, as no block tells the two apart. It reads a JSON or an event-stream response.
|
|
148
148
|
- A declared error, the action's own or a built-in one, is a typed failure of its decoded value: an `isError` result from the tool, or a 401 or 403 from the endpoint's authentication or an authorizer, whose body is the same JSON. Match it with `Effect.catchTag`, exactly as on the HTTP client.
|
|
149
|
-
- Any other answer fails with `McpCallError`, whose `message` holds it: another status, the native server's own text (invalid arguments, a defect), no reply, a result without `structuredContent
|
|
149
|
+
- Any other answer fails with `McpCallError`, whose `message` holds it: another status, the native server's own text (invalid arguments, a defect), no reply, a result without the `structuredContent` its success is sent as, or a JSON-RPC error (an unknown tool). Match it with `Effect.catchTag("McpCallError", ...)`. A success that does not decode, such as one missing the blocks of its media fields, fails with `SchemaError`, as it does from `ActionHttp.client`.
|
|
150
150
|
- The input is typed, so a malformed call cannot be sent through a client method. To assert on a malformed call, another MCP method or the response itself, such as a refusal's status and `WWW-Authenticate` challenge, send `mcpRequest(method, params, options)`.
|
|
151
151
|
- `mcpRequest` is one stateless request as a client method sends it, a native `HttpClientRequest` the test sends itself: with `mcp-name` from `params.uri` for `resources/read` and from `params.name` otherwise, and the client metadata in `_meta`. A `_meta` in `params`, such as a `progressToken`, is merged over the client metadata, and the protocol version is always the request's own.
|
|
152
152
|
- Under `layer`, send it with `HttpClient.execute(Testing.mcpRequest("tools/list"))`, which succeeds with the response whatever its status.
|
package/docs/guarantees.md
CHANGED
|
@@ -40,17 +40,17 @@ built-in errors are `Action.InvalidInput` (400), `Action.Unauthenticated` (401)
|
|
|
40
40
|
`Action.Forbidden` (403), each `{ _tag, message }`; an `InvalidInput` also lists `issues`, empty
|
|
41
41
|
when it names none, and a `Forbidden` may add `scopes`.
|
|
42
42
|
|
|
43
|
-
| Case | HTTP | MCP
|
|
44
|
-
| --------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
45
|
-
| Input | decoded with the schema's JSON codec; failure is **400** `InvalidInput`, `message` the schema issues | decoded with the schema's JSON codec; failure is an `isError` result from 2025-11-25 on, and a JSON-RPC `InvalidParams` error before
|
|
46
|
-
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: <encoded>` and its JSON as text on 2026-07-28; earlier stdio revisions structure only an object ([ActionMcp.md](ActionMcp.md#rules))
|
|
47
|
-
| Declared error | encoded by its schema with its `httpApiStatus` (unannotated: 422): `{"_tag":"UserNotFound","id":"x"}` | `isError: true`; text content is the same JSON; no `structuredContent`
|
|
48
|
-
| Authentication | a protected route without a valid credential: **401** with a challenge, before its body is read | a request a protected tool's call or a native feature needs: **401** with a challenge, before its body is read ([ActionMcp.md](ActionMcp.md#rules))
|
|
49
|
-
| Authorizer refusal | its JSON with its status, **401** / **403**, after input decoding; the handler never runs | the same `isError` result, after arguments are decoded; on a Bearer-authenticated endpoint a protected tool's step-up refusal is **401** / **403**
|
|
50
|
-
| Invalid output encoding |
|
|
51
|
-
| Defect | empty **500** | same generic `isError` result; cause logged, not sent
|
|
52
|
-
| Unknown path or method | 404 | another path: 404, as HTTP; another HTTP method on the endpoint: an empty **405** with `Allow: POST`; an unknown tool: a JSON-RPC `-32602` error, with 200 over HTTP
|
|
53
|
-
| Invalid JSON / content type | 400 `InvalidInput` / 415 for a missing or non-JSON type, after authentication on a protected route | over HTTP, a JSON-RPC `Parse error` (`-32700`) with 200 / an empty **415** for a missing or non-JSON type; over stdio, a line that is not JSON goes unanswered
|
|
43
|
+
| Case | HTTP | MCP |
|
|
44
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| Input | decoded with the schema's JSON codec; failure is **400** `InvalidInput`, `message` the schema issues | decoded with the schema's JSON codec; failure is an `isError` result from 2025-11-25 on, and a JSON-RPC `InvalidParams` error before |
|
|
46
|
+
| Success | the success codec's encoding as the body: `{"id":"1","name":"Ada"}`, `42` | `structuredContent: <encoded>` and its JSON as text on 2026-07-28; earlier stdio revisions structure only an object ([ActionMcp.md](ActionMcp.md#rules)); a media field is lifted into a block of its own ([ActionMcp.md](ActionMcp.md#media)) |
|
|
47
|
+
| Declared error | encoded by its schema with its `httpApiStatus` (unannotated: 422): `{"_tag":"UserNotFound","id":"x"}` | `isError: true`; text content is the same JSON; no `structuredContent` |
|
|
48
|
+
| Authentication | a protected route without a valid credential: **401** with a challenge, before its body is read | a request a protected tool's call or a native feature needs: **401** with a challenge, before its body is read ([ActionMcp.md](ActionMcp.md#rules)) |
|
|
49
|
+
| Authorizer refusal | its JSON with its status, **401** / **403**, after input decoding; the handler never runs | the same `isError` result, after arguments are decoded; on a Bearer-authenticated endpoint a protected tool's step-up refusal is **401** / **403** |
|
|
50
|
+
| Invalid output encoding | an empty **500**, reported to `ErrorReporter`s | `isError: true`, text `Tool execution failed due to an internal server error.`; cause logged, not sent |
|
|
51
|
+
| Defect | empty **500** | same generic `isError` result; cause logged, not sent |
|
|
52
|
+
| Unknown path or method | 404 | another path: 404, as HTTP; another HTTP method on the endpoint: an empty **405** with `Allow: POST`; an unknown tool: a JSON-RPC `-32602` error, with 200 over HTTP |
|
|
53
|
+
| Invalid JSON / content type | 400 `InvalidInput` / 415 for a missing or non-JSON type, after authentication on a protected route | over HTTP, a JSON-RPC `Parse error` (`-32700`) with 200 / an empty **415** for a missing or non-JSON type; over stdio, a line that is not JSON goes unanswered |
|
|
54
54
|
|
|
55
55
|
- The HTTP surfaces set no header of their own; handler failures are ordinary declared error responses. On a route a Bearer descriptor authenticates, a `Forbidden` naming scopes carries `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`.
|
|
56
56
|
- Authentication marks every response to a request it authenticates, on a protected HTTP route, an MCP endpoint naming a descriptor or a route of the host's own under `Authentication.protect`, `Cache-Control: no-store`, unless its route states its own caching, such as `private, max-age=31536000, immutable` for a content-addressed download. A covered route stating its own says `private` unless its answer is the same for every caller: a credential a proxy forwards is invisible to a cache in front of that proxy, which may store the answer and serve it to anyone. A failure that enclosing middleware serializes always gets `no-store`, whatever caching it states. An MCP endpoint's answer to a request it does not authenticate, discovery, its listing or a public tool's call without a credential, gets none, as a public HTTP route's does: it reads no credential. It gives every 401 among them without a challenge its scheme's, where the scheme has one, whether authentication, the authorizer or a handler answers it, a failure that enclosing middleware serializes included. As a protected resource, it names its metadata URL in every challenge, and a 401's names `scopesRequired`, and `invalid_token` when the request presented credentials ([Authentication.md](Authentication.md#rules)).
|
|
@@ -62,7 +62,8 @@ when it names none, and a `Forbidden` may add `scopes`.
|
|
|
62
62
|
- HTTP refuses undeclared input fields, nested ones too, as the OpenAPI document's `additionalProperties: false` says: the request is a 400 `InvalidInput` naming the field's path. A client drops them when it encodes, since TypeScript lets a wider value through. A record accepts every key its key schema does. A CLI command refuses them too, with an `InvalidInput`, in `--input` and in a flag's JSON. A Toolkit drops them, as Effect decodes a model's tool call. An action without input, `input: {}` given or not, accepts only `{}`, on every surface; over HTTP, an empty JSON body is a 400. MCP tools are strict (`Tool.Strict`): undeclared arguments are invalid arguments, an `isError` result from 2025-11-25 on and a JSON-RPC `InvalidParams` error on earlier stdio revisions. An input that declares its fields publishes `additionalProperties: false` in its schema over HTTP and on MCP ([ActionMcp.md](ActionMcp.md#rules)).
|
|
63
63
|
- An `InvalidInput` for input that does not decode lists `issues`, one `{ path, message }` per issue its `message` describes: `path` holds the keys and indexes from the input's root, `["lines", 1, "count"]`, a symbol key as its string form, `Symbol(name)`, and is empty for the input as a whole. Over HTTP a wrong or missing top-level literal field is such an issue, reported as the whole expected shape. A handler failing with `InvalidInput` may give `issues` of its own, sent as written, and `issues: []` without them: the field is always sent, and the OpenAPI document requires it. MCP's and a Toolkit's native answers have a message only.
|
|
64
64
|
- A message for input that does not decode says what the schema expects and, where Effect's decoder reports one, each issue's path, as does each of an `InvalidInput`'s `issues`; never a value sent, on every surface: `InvalidInput` over HTTP, from a CLI command and from `Action.client`, MCP's invalid-arguments result, a Toolkit's `ToolParameterValidationError`. A path names each key it passes through, an undeclared key and a record's keys included, so a record keyed by an email or a token names it. Effect CLI's own parser, which reads a flag before the schema decodes it, names a rejected choice, an unknown flag or an unexpected argument as typed. A schema's own `message` annotation, or a filter's message, is sent as written, so never put the input in one. Declare a secret `Schema.Redacted`, so what a handler logs of its decoded input prints `<redacted>`; a tool call's span holds it all the same ([observability](#observability)).
|
|
65
|
-
- A
|
|
65
|
+
- A media field, `Action.Image`, is JSON on every surface but MCP: `{ "data": "<base64>", "mimeType": "image/png" }`, the bytes in base64, as Effect's JSON codec encodes a `Uint8Array`. HTTP sends it so, a CLI command prints it so, a Toolkit gives it so to a model, and `Action.client` passes it through that codec. Only an MCP tool lifts it, into an image block beside the rest of the success ([ActionMcp.md](ActionMcp.md#media)).
|
|
66
|
+
- A CLI command prints the success's encoding on stdout, and a failure on stderr as the JSON HTTP sends: the action's declared error, a binding's, a built-in one, `InvalidInput` included. A success that does not encode is a defect there, which the command does not print ([ActionCli.md](ActionCli.md#rules)).
|
|
66
67
|
- An `Action.client` call passes its input, its success and its failure through their JSON codecs, encoded then decoded, as HTTP carries them, so its handler and its caller get what a remote call's would. A success or a failure that does not pass, an error its action does not declare included, is a defect, as it is an empty 500 over HTTP ([Action.md](Action.md#clients)).
|
|
67
68
|
- A failure an action does not declare, which the types refuse but plain JavaScript or a cast lets through, is answered by each surface its own way: `Action.client` dies with a defect, HTTP answers an empty 500, MCP its internal-error tool result, `Tool execution failed due to an internal server error.`, a local CLI command fails with `UserError` whose `cause` is that failure, and a Toolkit call fails with an `AiError`, `Failed to encode result`. Declare every error a handler may fail with.
|
|
68
69
|
|
package/docs/setup.md
CHANGED
|
@@ -5,12 +5,17 @@ Installation, version pins, entry points, a minimal program, and package boundar
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pnpm add @gjermundgaraba/effect-actions effect@~4.0.
|
|
8
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.2
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
| Need | Add |
|
|
12
12
|
| ----------------------------------- | ------------------------------ |
|
|
13
|
-
| Node HTTP server, stdio host or CLI | `@effect/platform-node@~4.0.
|
|
13
|
+
| Node HTTP server, stdio host or CLI | `@effect/platform-node@~4.0.2` |
|
|
14
|
+
|
|
15
|
+
The `effect` peer admits the patches of the Effect release the package is built and tested
|
|
16
|
+
against, and no later minor: Effect marks the HTTP, HTTP API, AI and CLI modules the surfaces build
|
|
17
|
+
on unstable, so a minor release may change them. A release of this package admits a later minor
|
|
18
|
+
once it is tested against it.
|
|
14
19
|
|
|
15
20
|
Node `^22.12.0 || ^24.0.0 || >=26.0.0`. TypeScript 7 or newer; earlier versions are not
|
|
16
21
|
supported. ESM only.
|
|
@@ -95,7 +100,7 @@ the browser example in [ActionMcp.md](ActionMcp.md#cross-origin-browsers).
|
|
|
95
100
|
|
|
96
101
|
`Action`, `ActionHttp` and `Authentication`, and every module they import, import Effect through three
|
|
97
102
|
specifiers alone: `effect`, `effect/http` and `effect/http-api`. A page that loads Effect from
|
|
98
|
-
an import map rather than its bundle maps those three. A map pointing at Effect
|
|
103
|
+
an import map rather than its bundle maps those three. A map pointing at Effect's
|
|
99
104
|
published files maps `effect/Cause`, `effect/Effect`, `effect/Exit` and `effect/Function` too,
|
|
100
105
|
to the same files the `effect` barrel loads, as Effect's own `Runtime` module imports them by
|
|
101
106
|
name; a build or CDN that resolves Effect's own imports needs only the three.
|
|
@@ -122,7 +127,7 @@ What the package does and does not do: [guarantees.md](guarantees.md#scope).
|
|
|
122
127
|
- A browser build fails with `Could not resolve "node:…"`, or warns `Module "node:…" has been externalized for browser compatibility`: the page imports a module that also holds server code, such as a contract beside its `Action.implement`, or an authentication descriptor beside its `Authentication.layer` verifier. Move the contracts, their identities, the descriptor and the binding to modules that import no server code.
|
|
123
128
|
- A page that loads Effect from an import map still bundles a second copy of Effect, or of some of its modules: a module of the page imports an Effect specifier the map does not serve, such as `effect/Schema`. Serve `effect`, `effect/http` and `effect/http-api`, all `Action`, `ActionHttp` and `Authentication` import, and import Effect through them in the page's own modules too.
|
|
124
129
|
- A page loading Effect from an import map fails before its modules run, with `Failed to resolve module specifier "effect/Cause"` in Chromium: the map points at Effect's published files and lacks a specifier Effect imports itself. Map `effect/Cause`, `effect/Effect`, `effect/Exit` and `effect/Function` to the same files, or serve Effect from a build that resolves its own imports.
|
|
125
|
-
- Type errors inside `effect/*` modules after install: `effect` version drift. Install one `effect`
|
|
126
|
-
- An unmet peer warning for `effect
|
|
130
|
+
- Type errors inside `effect/*` modules after install: `effect` version drift. Install one `effect` release, in the peer range, for every package.
|
|
131
|
+
- An unmet peer warning for `effect`, such as `found 4.1.0`: the installed Effect is outside the releases this one is tested against. Install the `effect` range [Install](#install) names, with every `@effect/*` package on the same release.
|
|
127
132
|
- `Cannot find module 'effect/unstable/http'` (or `…/httpapi`, `…/cli`, `…/ai`): Effect `4.0.0-rc.118` moved these modules to `effect/http`, `effect/http-api`, `effect/cli` and `effect/ai`. Import the new paths.
|
|
128
|
-
- `Cannot find module 'effect/http'` (or `effect/http-api`, `effect/cli`, `effect/ai`), or at run time `Cannot find module '…/node_modules/effect/dist/http-api.js'` (or `http.js`, `ai.js`, `cli.js`): `effect` is a release candidate older than `4.0.0-rc.118`, which moved these modules out of `effect/unstable/*`. Install `effect`
|
|
133
|
+
- `Cannot find module 'effect/http'` (or `effect/http-api`, `effect/cli`, `effect/ai`), or at run time `Cannot find module '…/node_modules/effect/dist/http-api.js'` (or `http.js`, `ai.js`, `cli.js`): `effect` is a release candidate older than `4.0.0-rc.118`, which moved these modules out of `effect/unstable/*`. Install the `effect` range [Install](#install) names.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Define Effect action contracts once and project them onto HTTP, MCP, native Toolkits, and CLIs.",
|
|
5
5
|
"homepage": "https://github.com/gjermundgaraba/effect-actions#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -31,19 +31,19 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@effect/platform-node": "4.0.
|
|
35
|
-
"@effect/vitest": "4.0.
|
|
36
|
-
"@modelcontextprotocol/client": "2.
|
|
37
|
-
"@types/node": "26.6.
|
|
38
|
-
"effect": "4.0.
|
|
34
|
+
"@effect/platform-node": "4.0.2",
|
|
35
|
+
"@effect/vitest": "4.0.2",
|
|
36
|
+
"@modelcontextprotocol/client": "2.3.1",
|
|
37
|
+
"@types/node": "26.6.4",
|
|
38
|
+
"effect": "4.0.2",
|
|
39
39
|
"tsx": "4.23.15",
|
|
40
40
|
"typescript": "7.0.2",
|
|
41
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
42
|
-
"vite-plus": "1.
|
|
43
|
-
"vitest": "5.0.
|
|
41
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
42
|
+
"vite-plus": "1.1.0",
|
|
43
|
+
"vitest": "5.0.3"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"effect": "~4.0.
|
|
46
|
+
"effect": "~4.0.2"
|
|
47
47
|
},
|
|
48
48
|
"devEngines": {
|
|
49
49
|
"packageManager": {
|