@gjermundgaraba/effect-actions 0.14.0 → 0.15.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/dist/Action.d.mts +1 -1
- package/dist/ActionCli.d.mts +4 -6
- package/dist/ActionCli.mjs +11 -9
- package/dist/ActionHttp.mjs +18 -30
- package/dist/ActionMcp.d.mts +16 -7
- package/dist/ActionMcp.mjs +76 -89
- package/dist/ActionRpc.mjs +17 -29
- package/dist/ActionToolkit.mjs +26 -11
- package/dist/Authentication.mjs +1 -2
- package/dist/Testing.d.mts +6 -73
- package/dist/Testing.mjs +3 -102
- package/dist/authentication/provider.mjs +1 -6
- package/dist/authentication/refusal.mjs +14 -4
- package/dist/cli/commands.mjs +8 -8
- package/dist/contract/call.d.mts +2 -1
- package/dist/contract/call.mjs +14 -1
- package/dist/contract/errors.d.mts +2 -3
- package/dist/contract/errors.mjs +6 -12
- package/dist/contract/implementation.mjs +33 -5
- package/dist/contract/rules.d.mts +7 -0
- package/dist/contract/rules.mjs +50 -3
- package/dist/http/client.mjs +4 -7
- package/dist/mcp/client.d.mts +87 -0
- package/dist/mcp/client.mjs +122 -0
- package/dist/mcp/protocol.mjs +25 -37
- package/docs/Action.md +6 -5
- package/docs/ActionCli.md +4 -3
- package/docs/ActionMcp-failures.md +6 -2
- package/docs/ActionMcp.md +30 -33
- package/docs/ActionToolkit-failures.md +2 -1
- package/docs/ActionToolkit.md +2 -2
- package/docs/Authentication-failures.md +2 -2
- package/docs/Authentication.md +10 -5
- package/docs/CONTEXT.md +1 -1
- package/docs/Testing-failures.md +3 -3
- package/docs/Testing.md +16 -24
- package/docs/guarantees.md +15 -14
- package/examples/kitchen-sink/README.md +26 -26
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +3 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +13 -6
- package/examples/kitchen-sink/test/routes.test.ts +4 -3
- package/examples/live-rpc/README.md +7 -2
- package/examples/mcp-tools/test/routes.test.ts +7 -6
- package/examples/notes-service/README.md +13 -3
- package/examples/notes-service/src/notes/Notes.ts +4 -2
- package/examples/notes-service/src/routes.ts +24 -14
- package/examples/notes-service/src/worker.ts +20 -0
- package/examples/notes-service/test/commands.test.ts +2 -1
- package/examples/notes-service/test/routes.test.ts +6 -3
- package/examples/notes-service/test/worker.test.ts +34 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +2 -1
- package/package.json +1 -1
- package/dist/http/declared.mjs +0 -20
- package/dist/mcp/protocol.d.mts +0 -18
- package/dist/toolkit/tools.mjs +0 -29
|
@@ -3,6 +3,7 @@ import { Effect, Layer } from "effect";
|
|
|
3
3
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
4
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
5
5
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
6
|
+
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
6
7
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
7
8
|
import { Http } from "../src/Api.js";
|
|
8
9
|
import { Notes } from "../src/notes/Notes.js";
|
|
@@ -11,7 +12,9 @@ import { routes } from "../src/routes.js";
|
|
|
11
12
|
import { GetStatus } from "../src/status/Status.js";
|
|
12
13
|
|
|
13
14
|
// What the wire adds: the real routes, behind their real authentication, answering in memory.
|
|
14
|
-
// No port is opened. The caller is the token each client sends, as in production.
|
|
15
|
+
// No port is opened. The caller is the token each client sends, as in production. A service a
|
|
16
|
+
// builder yields is provided here, as server.ts provides it: one left out is a `Scope` error at
|
|
17
|
+
// every `it.effect` (docs/Testing-failures.md).
|
|
15
18
|
const served = Testing.layer(routes).pipe(Layer.provide(NoteStore.layerMemory));
|
|
16
19
|
|
|
17
20
|
const as = (token: string) => ({
|
|
@@ -65,7 +68,7 @@ describe("the service, over HTTP and MCP", () => {
|
|
|
65
68
|
it.effect("serves one store to both surfaces: a note written over HTTP is read as a tool", () =>
|
|
66
69
|
Effect.gen(function* () {
|
|
67
70
|
const http = yield* ActionHttp.client(Http, as("ada"));
|
|
68
|
-
const mcp = yield*
|
|
71
|
+
const mcp = yield* ActionMcp.client([GetStatus, ...Notes], as("ada"));
|
|
69
72
|
const note = yield* http.createNote({ title: "Plan", body: "Draft" });
|
|
70
73
|
|
|
71
74
|
expect(yield* mcp.getNote({ id: note.id })).toEqual(note);
|
|
@@ -75,7 +78,7 @@ describe("the service, over HTTP and MCP", () => {
|
|
|
75
78
|
|
|
76
79
|
it.effect("lists every tool to a signed-out MCP client, as one of them is public", () =>
|
|
77
80
|
Effect.gen(function* () {
|
|
78
|
-
const response = yield* HttpClient.execute(
|
|
81
|
+
const response = yield* HttpClient.execute(ActionMcp.request("tools/list"));
|
|
79
82
|
|
|
80
83
|
expect(response.status).toBe(200);
|
|
81
84
|
expect(yield* response.text).toContain('"name":"createNote"');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
5
|
+
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
6
|
+
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
7
|
+
import { CreateNote, GetNote } from "../src/notes/Notes.js";
|
|
8
|
+
import { GetStatus } from "../src/status/Status.js";
|
|
9
|
+
import worker from "../src/worker.js";
|
|
10
|
+
|
|
11
|
+
// What a host calling the worker's `fetch` gets: the module's own handler, answering in memory.
|
|
12
|
+
const served = Testing.layer(worker.fetch);
|
|
13
|
+
|
|
14
|
+
const as = (token: string) => ({
|
|
15
|
+
transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token)),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("the worker", () => {
|
|
19
|
+
it.effect("serves the tools, each request as the caller it authenticates", () =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const anyone = yield* ActionMcp.client([GetStatus, GetNote]);
|
|
22
|
+
const ada = yield* ActionMcp.client([CreateNote, GetNote], as("ada"));
|
|
23
|
+
|
|
24
|
+
expect(yield* anyone.getStatus()).toMatchObject({ service: "notes-service" });
|
|
25
|
+
|
|
26
|
+
const note = yield* ada.createNote({ title: "Plan", body: "First draft" });
|
|
27
|
+
|
|
28
|
+
expect(yield* ada.getNote({ id: note.id })).toEqual(note);
|
|
29
|
+
expect(yield* Effect.flip(anyone.getNote({ id: note.id }))).toBeInstanceOf(
|
|
30
|
+
Action.Unauthenticated,
|
|
31
|
+
);
|
|
32
|
+
}).pipe(Effect.provide(served)),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, it } from "@effect/vitest";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
4
|
+
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
4
5
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
5
6
|
import { Http } from "../../src/Api.js";
|
|
6
7
|
import { Greet } from "../../src/greeting/Greeting.js";
|
|
@@ -10,7 +11,7 @@ import { routes } from "../../src/routes.js";
|
|
|
10
11
|
it.effect("greets over HTTP and as an MCP tool", () =>
|
|
11
12
|
Effect.gen(function* () {
|
|
12
13
|
const http = yield* ActionHttp.client(Http);
|
|
13
|
-
const mcp = yield*
|
|
14
|
+
const mcp = yield* ActionMcp.client([Greet]);
|
|
14
15
|
|
|
15
16
|
expect(yield* http.greet({ name: "Ada" })).toBe("Hello, Ada!");
|
|
16
17
|
expect(yield* mcp.greet({ name: "Ada" })).toBe("Hello, Ada!");
|
package/package.json
CHANGED
package/dist/http/declared.mjs
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { unsuspended } from "../contract/rules.mjs";
|
|
2
|
-
import { Schema, SchemaAST } from "effect";
|
|
3
|
-
import { HttpServerResponse } from "effect/http";
|
|
4
|
-
import { HttpApiSchema } from "effect/http-api";
|
|
5
|
-
//#region src/http/declared.ts
|
|
6
|
-
const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
|
|
7
|
-
const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
|
|
8
|
-
const declared = (error) => {
|
|
9
|
-
const status = statusThroughSuspensions(error.ast);
|
|
10
|
-
if (status !== void 0) return [httpApiStatusOf(error.ast) === void 0 ? HttpApiSchema.status(status)(error) : error];
|
|
11
|
-
const resolved = unsuspended(error.ast);
|
|
12
|
-
if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
|
|
13
|
-
return [HttpApiSchema.status(422)(error)];
|
|
14
|
-
};
|
|
15
|
-
const declaredResponse = (errors, error) => {
|
|
16
|
-
const schema = errors.flatMap(declared).find((each) => Schema.is(each)(error));
|
|
17
|
-
return schema === void 0 ? void 0 : HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: httpApiStatusOf(schema.ast) ?? 422 });
|
|
18
|
-
};
|
|
19
|
-
//#endregion
|
|
20
|
-
export { declared, declaredResponse };
|
package/dist/mcp/protocol.d.mts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import "../contract/implementation.mjs";
|
|
2
|
-
import "../contract/media.mjs";
|
|
3
|
-
import "../Action.mjs";
|
|
4
|
-
import { Schema } from "effect";
|
|
5
|
-
import { McpProtocol } from "effect/ai";
|
|
6
|
-
//#region src/mcp/protocol.d.ts
|
|
7
|
-
/**
|
|
8
|
-
* An MCP request's parameters, as `Testing.mcpRequest` takes them: JSON, with any `_meta`, leaving
|
|
9
|
-
* out an `undefined` one, as a caller may. A helper passing on whole params types them as this,
|
|
10
|
-
* and a tool's `arguments` as `Schema.Json`, never `unknown`.
|
|
11
|
-
*/
|
|
12
|
-
export interface Params {
|
|
13
|
-
readonly _meta?: {
|
|
14
|
-
readonly [key: string]: Schema.Json;
|
|
15
|
-
};
|
|
16
|
-
readonly [key: string]: Schema.Json | undefined;
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
package/dist/toolkit/tools.mjs
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { assertOnce, projectedErrors } from "../contract/rules.mjs";
|
|
2
|
-
import { acquire } from "../contract/implementation.mjs";
|
|
3
|
-
import { Effect, Schema } from "effect";
|
|
4
|
-
import { Tool, Toolkit } from "effect/ai";
|
|
5
|
-
//#region src/toolkit/tools.ts
|
|
6
|
-
const annotateIfGiven = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
|
|
7
|
-
const annotateFromContract = (tool, { readOnly, mcp }) => {
|
|
8
|
-
const destructiveUnlessReadOnly = mcp.destructiveHint ?? (readOnly ? false : void 0);
|
|
9
|
-
const meta = mcp._meta === void 0 ? void 0 : { ...mcp._meta };
|
|
10
|
-
const hinted = annotateIfGiven(annotateIfGiven(annotateIfGiven(tool.annotate(Tool.Readonly, readOnly), Tool.Destructive, destructiveUnlessReadOnly), Tool.Idempotent, mcp.idempotentHint), Tool.OpenWorld, mcp.openWorldHint);
|
|
11
|
-
return annotateIfGiven(annotateIfGiven(hinted, Tool.Title, mcp.title), Tool.Meta, meta);
|
|
12
|
-
};
|
|
13
|
-
const jsonToolOf = (action) => Tool.make(action.name, {
|
|
14
|
-
description: action.description,
|
|
15
|
-
parameters: Schema.toCodecJson(action.input),
|
|
16
|
-
success: Schema.toCodecJson(action.success),
|
|
17
|
-
failure: Schema.toCodecJson(Schema.Union(projectedErrors(action))),
|
|
18
|
-
failureMode: "return"
|
|
19
|
-
});
|
|
20
|
-
const bindTools = (apps, projection) => {
|
|
21
|
-
assertOnce(projection.label, apps.flatMap((app) => app.actions));
|
|
22
|
-
const toolkit = Toolkit.make(...apps.flatMap((app) => app.actions.map((action) => annotateFromContract(projection.tool(jsonToolOf(action), action, app), action))));
|
|
23
|
-
return {
|
|
24
|
-
toolkit,
|
|
25
|
-
layer: toolkit.toLayer(Effect.map(acquire(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, projection.handler?.(run, action) ?? run]))))
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
//#endregion
|
|
29
|
-
export { bindTools };
|