@gjermundgaraba/effect-actions 0.16.0 → 0.18.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 +3 -1
- package/dist/Action.d.mts +3 -5
- package/dist/Action.mjs +3 -1
- package/dist/ActionCli.d.mts +31 -42
- package/dist/ActionCli.mjs +6 -16
- package/dist/ActionHttp.d.mts +12 -8
- package/dist/ActionHttp.mjs +3 -1
- package/dist/ActionMcp.d.mts +1 -1
- package/dist/ActionMcp.mjs +4 -1
- package/dist/ActionRpc.d.mts +5 -3
- package/dist/ActionRpc.mjs +2 -1
- package/dist/ActionToolkit.d.mts +3 -2
- package/dist/ActionToolkit.mjs +3 -1
- package/dist/Authentication.d.mts +1 -1
- package/dist/Authentication.mjs +1 -1
- package/dist/authentication/provider.d.mts +2 -2
- package/dist/authentication/refusal.mjs +1 -1
- package/dist/cli/commands.d.mts +4 -3
- package/dist/cli/commands.mjs +6 -5
- package/dist/contract/ast.mjs +10 -0
- package/dist/contract/call.d.mts +1 -1
- package/dist/contract/caller.d.mts +9 -0
- package/dist/contract/caller.mjs +12 -0
- package/dist/contract/{rules.d.mts → declared.d.mts} +6 -10
- package/dist/contract/declared.mjs +42 -0
- package/dist/contract/hints.mjs +14 -0
- package/dist/contract/implementation.d.mts +4 -3
- package/dist/contract/implementation.mjs +4 -8
- package/dist/contract/names.mjs +25 -0
- package/dist/contract/status.mjs +18 -0
- package/dist/http/client.d.mts +19 -9
- package/dist/http/client.mjs +6 -1
- package/dist/mcp/client.d.mts +52 -12
- package/dist/mcp/client.mjs +62 -19
- package/dist/mcp/protocol.mjs +1 -1
- package/docs/Action.md +17 -3
- package/docs/ActionCli-failures.md +10 -9
- package/docs/ActionCli.md +22 -21
- package/docs/ActionHttp-failures.md +6 -0
- package/docs/ActionHttp.md +9 -4
- package/docs/ActionMcp.md +3 -3
- package/docs/ActionToolkit.md +25 -2
- package/docs/Authentication-failures.md +1 -1
- package/docs/CONTEXT.md +2 -1
- package/docs/README.md +4 -4
- package/docs/Testing.md +3 -1
- package/examples/README.md +10 -2
- package/examples/assistant/README.md +24 -20
- package/examples/assistant/src/agent.ts +11 -7
- package/examples/assistant/src/cli.ts +2 -2
- package/examples/assistant/src/platform/tasks-file.ts +38 -0
- package/examples/assistant/src/tasks/store.ts +6 -38
- package/examples/assistant/src/toolkit.ts +11 -1
- package/examples/assistant/test/{tasks/store.test.ts → platform/tasks-file.test.ts} +2 -1
- package/examples/kitchen-sink/src/cli.ts +19 -21
- package/examples/mcp-tools/README.md +14 -11
- package/examples/mcp-tools/src/{printers/fake.ts → platform/fake-farm.ts} +2 -2
- package/examples/mcp-tools/src/platform/print-farm.ts +118 -0
- package/examples/mcp-tools/src/printers/farm.ts +6 -122
- package/examples/mcp-tools/src/server.ts +4 -4
- package/examples/mcp-tools/src/stdio.ts +4 -4
- package/examples/mcp-tools/test/printers/printers.test.ts +3 -3
- package/examples/mcp-tools/test/routes.test.ts +4 -4
- package/examples/notes-service/README.md +34 -2
- package/examples/notes-service/src/cli.ts +3 -9
- package/examples/notes-service/src/commands.ts +40 -36
- package/examples/notes-service/src/identity/authorize.ts +1 -0
- package/examples/notes-service/src/notes/Notes.ts +22 -5
- package/examples/notes-service/src/notes/handlers.ts +2 -2
- package/examples/notes-service/src/notes/store.ts +36 -6
- package/examples/notes-service/test/commands.test.ts +13 -19
- package/examples/notes-service/test/notes/idempotency.test.ts +132 -0
- package/examples/notes-service/test/routes.test.ts +40 -1
- package/package.json +5 -5
- package/dist/_virtual/_rolldown/runtime.mjs +0 -13
- package/dist/contract/rules.mjs +0 -100
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Anyone, assertAuthentication } from "./caller.mjs";
|
|
1
2
|
import { Unauthenticated } from "./errors.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { assertDistinct, assertOnce } from "./names.mjs";
|
|
4
|
+
import { projectedErrors } from "./declared.mjs";
|
|
3
5
|
import { Array, Cause, Context, Effect, Exit, Layer, Option, Schema } from "effect";
|
|
4
6
|
//#region src/contract/implementation.ts
|
|
5
7
|
const uniqueKey = () => Math.random().toString(36).slice(2);
|
|
@@ -152,13 +154,7 @@ const dispatch = (action, handle, before) => {
|
|
|
152
154
|
} else reasons.push(reason);
|
|
153
155
|
return yield* Effect.failCause(Cause.fromReasons(reasons));
|
|
154
156
|
});
|
|
155
|
-
return (input) => {
|
|
156
|
-
const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), actionAttributes), action.name, {
|
|
157
|
-
captureStackTrace: false,
|
|
158
|
-
attributes: actionAttributes
|
|
159
|
-
});
|
|
160
|
-
return Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(handled), Effect.catchCause(declaredOrDefect)));
|
|
161
|
-
};
|
|
157
|
+
return (input) => Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(Effect.suspend(() => handle(input))), Effect.catchCause(declaredOrDefect))).pipe(Effect.annotateLogs(actionAttributes), Effect.withSpan(action.name, { attributes: actionAttributes }, { captureStackTrace: false }));
|
|
162
158
|
};
|
|
163
159
|
//#endregion
|
|
164
160
|
export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, servingOf, toList };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/contract/names.ts
|
|
2
|
+
const validName = /^[A-Za-z0-9_-]{1,128}$/;
|
|
3
|
+
const assertName = (what, name) => {
|
|
4
|
+
if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
|
|
5
|
+
};
|
|
6
|
+
const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
|
|
7
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const item of items) {
|
|
9
|
+
const name = nameOf(item);
|
|
10
|
+
const claimant = claimantOf(item);
|
|
11
|
+
const other = seen.get(name);
|
|
12
|
+
if (other !== void 0) {
|
|
13
|
+
const by = other === claimant ? "" : `, claimed by ${other} and ${claimant}`;
|
|
14
|
+
throw new Error(`Duplicate ${what}: ${name}${by}`);
|
|
15
|
+
}
|
|
16
|
+
seen.set(name, claimant);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const assertOnce = (what, actions) => assertDistinct(what, actions, (action) => action.name);
|
|
20
|
+
const assertKnown = (what, keys, names) => {
|
|
21
|
+
const unknown = keys.filter((key) => !names.includes(key));
|
|
22
|
+
if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")}`);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { assertDistinct, assertKnown, assertName, assertOnce };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { unsuspended } from "./ast.mjs";
|
|
2
|
+
import { Schema, SchemaAST } from "effect";
|
|
3
|
+
//#region src/contract/status.ts
|
|
4
|
+
const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
|
|
5
|
+
const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
|
|
6
|
+
/**
|
|
7
|
+
* `error` as an endpoint declares it: each member of a union without a status of its own apart,
|
|
8
|
+
* each with its status, 422 without one.
|
|
9
|
+
*/
|
|
10
|
+
const declared = (error) => {
|
|
11
|
+
const status = statusThroughSuspensions(error.ast);
|
|
12
|
+
if (status !== void 0) return [httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: status }) : error];
|
|
13
|
+
const resolved = unsuspended(error.ast);
|
|
14
|
+
if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
|
|
15
|
+
return [error.annotate({ httpApiStatus: 422 })];
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { declared, httpApiStatusOf };
|
package/dist/http/client.d.mts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { BuiltIns } from "../contract/errors.mjs";
|
|
2
2
|
import { Call } from "../contract/call.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { Any, Errors } from "../Action.mjs";
|
|
4
4
|
import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
|
|
5
5
|
import { Effect, Schema } from "effect";
|
|
6
6
|
import { HttpClient, HttpClientError } from "effect/http";
|
|
7
7
|
import { HttpApi, HttpApiClient } from "effect/http-api";
|
|
8
8
|
//#region src/http/client.d.ts
|
|
9
9
|
/**
|
|
10
|
-
* The native `HttpApiClient.make` options except `transformResponse
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The native `HttpApiClient.make` options except `transformResponse`, which may change a call's
|
|
11
|
+
* success, failure or services the method types cannot follow: `baseUrl`, and `transformClient`,
|
|
12
|
+
* which may return a client failing with `E` or requiring `R` of its own, as
|
|
13
|
+
* `HttpApiClient.makeWith` takes one.
|
|
13
14
|
*/
|
|
14
|
-
export
|
|
15
|
+
export interface Options<E = HttpClientError.HttpClientError, R = never> extends Pick<NonNullable<Parameters<typeof HttpApiClient.make>[1]>, "baseUrl"> {
|
|
16
|
+
/**
|
|
17
|
+
* Wraps the native `HttpClient` the calls send with: a bearer token is
|
|
18
|
+
* `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. A method also fails with what
|
|
19
|
+
* the returned client fails with beyond an `HttpClientError`, and requires what it requires.
|
|
20
|
+
*/
|
|
21
|
+
readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient.With<E, R>) | undefined;
|
|
22
|
+
}
|
|
15
23
|
type BindingError = Errors[number];
|
|
16
24
|
/**
|
|
17
25
|
* What one call of `A` fails with: a declared error value (the action's own, one `E` of the
|
|
@@ -20,7 +28,7 @@ type BindingError = Errors[number];
|
|
|
20
28
|
* the success does not decode. Input that does not encode is `InvalidInput`, and nothing is sent.
|
|
21
29
|
*/
|
|
22
30
|
export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
|
|
23
|
-
type Method<A extends Any, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E
|
|
31
|
+
type Method<A extends Any, E extends BindingError, CE, CR> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E> | CE, CR>>;
|
|
24
32
|
/**
|
|
25
33
|
* Any HTTP binding, with its actions erased: what `layer` and `client` read. The API is a native
|
|
26
34
|
* constraint here because native groups are invariant in their endpoints; `actions` carries the
|
|
@@ -35,7 +43,9 @@ export interface AnyHttp {
|
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* Every action of the binding `H`, as `client.<action>(input)`: a protected one may also fail
|
|
38
|
-
* with what its descriptor's verifier declares.
|
|
46
|
+
* with what its descriptor's verifier declares. Each call also fails with `E` and requires `R`,
|
|
47
|
+
* what its `transformClient` adds.
|
|
39
48
|
*/
|
|
40
|
-
export type Client<H extends AnyHttp> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]
|
|
41
|
-
//#endregion
|
|
49
|
+
export type Client<H extends AnyHttp, E = never, R = never> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>, E, R>; };
|
|
50
|
+
//#endregion
|
|
51
|
+
export {};
|
package/dist/http/client.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { checked } from "../contract/call.mjs";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/http";
|
|
3
4
|
import { HttpApiClient } from "effect/http-api";
|
|
4
5
|
//#region src/http/client.ts
|
|
5
|
-
const
|
|
6
|
+
const methodsWith = (http, httpClient, baseUrl) => Effect.map(HttpApiClient.makeWith(http.api, {
|
|
7
|
+
baseUrl,
|
|
8
|
+
httpClient
|
|
9
|
+
}), (client) => {
|
|
6
10
|
const native = client;
|
|
7
11
|
return Object.fromEntries(http.actions.map((action) => {
|
|
8
12
|
const method = native[action.name];
|
|
@@ -10,5 +14,6 @@ const methods = (http, options = {}) => Effect.map(HttpApiClient.make(http.api,
|
|
|
10
14
|
return [action.name, checked(action, (payload) => method({ payload }))];
|
|
11
15
|
}));
|
|
12
16
|
});
|
|
17
|
+
const methods = (http, { baseUrl, transformClient } = {}) => Effect.flatMap(HttpClient.HttpClient, (native) => transformClient === void 0 ? methodsWith(http, native, baseUrl) : methodsWith(http, transformClient(native), baseUrl));
|
|
13
18
|
//#endregion
|
|
14
19
|
export { methods };
|
package/dist/mcp/client.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BuiltIn } from "../contract/errors.mjs";
|
|
2
2
|
import { Call } from "../contract/call.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { Any } from "../Action.mjs";
|
|
4
4
|
import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
|
|
5
5
|
import { Effect, Schema } from "effect";
|
|
6
6
|
import { Headers, HttpClient, HttpClientError, HttpClientRequest } from "effect/http";
|
|
@@ -16,8 +16,11 @@ export interface Params {
|
|
|
16
16
|
};
|
|
17
17
|
readonly [key: string]: Schema.Json | undefined;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Where `client` sends, through what client, and what the endpoint's authentication declares: the
|
|
21
|
+
* client `transformClient` returns may fail with `E` and require `R`.
|
|
22
|
+
*/
|
|
23
|
+
export interface ClientOptions<D extends Any$1 | undefined = Any$1 | undefined, E = HttpClientError.HttpClientError, R = never> {
|
|
21
24
|
/**
|
|
22
25
|
* The endpoint, resolved by the `HttpClient`: relative under `Testing.layer`. Defaults to `/mcp`,
|
|
23
26
|
* the default `layerHttp` path.
|
|
@@ -27,7 +30,7 @@ export interface ClientOptions<D extends Any$1 | undefined = Any$1 | undefined>
|
|
|
27
30
|
* Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token is
|
|
28
31
|
* `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`.
|
|
29
32
|
*/
|
|
30
|
-
readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient
|
|
33
|
+
readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient.With<E, R>;
|
|
31
34
|
/**
|
|
32
35
|
* The endpoint's descriptor, as `layerHttp` takes it: an error its verifier declares, the
|
|
33
36
|
* endpoint's HTTP answer for a protected tool, is then that error's decoded value, as over
|
|
@@ -45,20 +48,55 @@ export interface RequestOptions {
|
|
|
45
48
|
readonly url?: string;
|
|
46
49
|
readonly headers?: Headers.Input;
|
|
47
50
|
}
|
|
51
|
+
declare const StatusError_base: Schema.Class<StatusError, Schema.TaggedStruct<"StatusError", {
|
|
52
|
+
readonly status: Schema.Int;
|
|
53
|
+
readonly body: Schema.String;
|
|
54
|
+
}>, import("effect/Cause").YieldableError>;
|
|
55
|
+
/** The endpoint answered with an HTTP status other than 200, and a body no declared error decodes. */
|
|
56
|
+
declare class StatusError extends StatusError_base {}
|
|
57
|
+
declare const ProtocolError_base: Schema.Class<ProtocolError, Schema.TaggedStruct<"ProtocolError", {
|
|
58
|
+
readonly code: Schema.Finite;
|
|
59
|
+
readonly message: Schema.String;
|
|
60
|
+
}>, import("effect/Cause").YieldableError>;
|
|
61
|
+
/**
|
|
62
|
+
* The endpoint answered with a JSON-RPC error, such as the native server's for invalid arguments
|
|
63
|
+
* or an unknown tool.
|
|
64
|
+
*/
|
|
65
|
+
declare class ProtocolError extends ProtocolError_base {}
|
|
66
|
+
declare const NoReply_base: Schema.Class<NoReply, Schema.TaggedStruct<"NoReply", {
|
|
67
|
+
readonly body: Schema.String;
|
|
68
|
+
}>, import("effect/Cause").YieldableError>;
|
|
69
|
+
/** The endpoint answered 200 without a JSON-RPC reply to read, as JSON or as an event stream. */
|
|
70
|
+
declare class NoReply extends NoReply_base {}
|
|
71
|
+
declare const UndeclaredError_base: Schema.Class<UndeclaredError, Schema.TaggedStruct<"UndeclaredError", {
|
|
72
|
+
readonly text: Schema.String;
|
|
73
|
+
}>, import("effect/Cause").YieldableError>;
|
|
74
|
+
/** The tool returned an error whose text decodes as none of the action's declared errors. */
|
|
75
|
+
declare class UndeclaredError extends UndeclaredError_base {}
|
|
76
|
+
declare const NoStructuredContent_base: Schema.Class<NoStructuredContent, Schema.TaggedStruct<"NoStructuredContent", {
|
|
77
|
+
readonly body: Schema.String;
|
|
78
|
+
}>, import("effect/Cause").YieldableError>;
|
|
79
|
+
/** The tool returned a result without the structured content its action's success needs. */
|
|
80
|
+
declare class NoStructuredContent extends NoStructuredContent_base {}
|
|
48
81
|
declare const McpCallError_base: Schema.Class<McpCallError, Schema.TaggedStruct<"McpCallError", {
|
|
82
|
+
readonly tool: Schema.String;
|
|
83
|
+
readonly reason: Schema.Union<readonly [typeof StatusError, typeof ProtocolError, typeof NoReply, typeof UndeclaredError, typeof NoStructuredContent]>;
|
|
49
84
|
readonly message: Schema.String;
|
|
50
85
|
}>, import("effect/Cause").YieldableError>;
|
|
51
86
|
/**
|
|
52
|
-
* An answer a client method cannot read as the action's success or a declared error
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* decode as the success is a `SchemaError` instead,
|
|
56
|
-
* `HttpClientError`.
|
|
87
|
+
* An answer a client method cannot read as the action's success or a declared error: `reason`
|
|
88
|
+
* says which, with what the endpoint sent, so a caller branches on it with
|
|
89
|
+
* `Effect.catchReason("McpCallError", "StatusError", ...)`. `message` names the tool and the
|
|
90
|
+
* reason. Structured content that does not decode as the success is a `SchemaError` instead,
|
|
91
|
+
* and a request that fails an `HttpClientError`.
|
|
57
92
|
*/
|
|
58
93
|
export declare class McpCallError extends McpCallError_base {}
|
|
59
94
|
type CallError<A extends Any, D> = A["error"][number]["Type"] | VerifierError<A, D>["Type"] | BuiltIn | Schema.SchemaError | HttpClientError.HttpClientError | McpCallError;
|
|
60
|
-
/**
|
|
61
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Every action of `Actions` as `client.<action>(input)`, calling its tool. Each call also fails
|
|
97
|
+
* with `E` and requires `R`, what its `transformClient` adds.
|
|
98
|
+
*/
|
|
99
|
+
export type Client<Actions extends ReadonlyArray<Any>, D = undefined, E = never, R = never> = { readonly [A in Actions[number] as A["name"]]: Call<A, Effect.Effect<A["success"]["Type"], CallError<A, D> | E, R>>; };
|
|
62
100
|
/**
|
|
63
101
|
* A client of an MCP endpoint served by `layerHttp`, one method per action calling
|
|
64
102
|
* its tool with one stateless request, as `ActionHttp.client` calls routes: the input is
|
|
@@ -71,6 +109,7 @@ export type Client<Actions extends ReadonlyArray<Any>, D = undefined> = { readon
|
|
|
71
109
|
* native `HttpClient`, such as the one `Testing.layer` provides.
|
|
72
110
|
*/
|
|
73
111
|
export declare function client<const Actions extends ReadonlyArray<Any>, D extends Any$1 | undefined = undefined>(actions: Actions, options?: ClientOptions<D>): Effect.Effect<Client<Actions, D>, never, HttpClient.HttpClient>;
|
|
112
|
+
export declare function client<const Actions extends ReadonlyArray<Any>, E, R, D extends Any$1 | undefined = undefined>(actions: Actions, options: ClientOptions<D, E, R>): Effect.Effect<Client<Actions, D, E, R>, never, HttpClient.HttpClient>;
|
|
74
113
|
/**
|
|
75
114
|
* One stateless MCP request of `method` with `params`, as `layerHttp` serves it,
|
|
76
115
|
* as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
|
|
@@ -84,4 +123,5 @@ export declare function client<const Actions extends ReadonlyArray<Any>, D exten
|
|
|
84
123
|
* a call its types would not send.
|
|
85
124
|
*/
|
|
86
125
|
export declare const request: (method: string, params?: Params, { headers, url }?: RequestOptions) => HttpClientRequest.HttpClientRequest;
|
|
87
|
-
//#endregion
|
|
126
|
+
//#endregion
|
|
127
|
+
export {};
|
package/dist/mcp/client.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { Anyone } from "../contract/caller.mjs";
|
|
1
2
|
import { Refusal } from "../contract/errors.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { assertOnce } from "../contract/names.mjs";
|
|
4
|
+
import { projectedErrors } from "../contract/declared.mjs";
|
|
3
5
|
import { checked } from "../contract/call.mjs";
|
|
4
6
|
import { defaultPath, httpProtocol, liftOf, unliftedFrom } from "./protocol.mjs";
|
|
5
|
-
import { Effect, Option, Predicate, Schema
|
|
7
|
+
import { Effect, Match, Option, Predicate, Schema } from "effect";
|
|
6
8
|
import { McpSchema } from "effect/ai";
|
|
7
9
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
8
10
|
import { Sse } from "effect/encoding";
|
|
@@ -42,14 +44,56 @@ const statelessRequest = (method, params) => {
|
|
|
42
44
|
}
|
|
43
45
|
};
|
|
44
46
|
};
|
|
47
|
+
/** The endpoint answered with an HTTP status other than 200, and a body no declared error decodes. */
|
|
48
|
+
var StatusError = class extends Schema.TaggedError()("StatusError", {
|
|
49
|
+
status: Schema.Int,
|
|
50
|
+
body: Schema.String
|
|
51
|
+
}) {};
|
|
45
52
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* its structured content. Its message holds the answer. Structured content that does not
|
|
49
|
-
* decode as the success is a `SchemaError` instead, and a request that fails an
|
|
50
|
-
* `HttpClientError`.
|
|
53
|
+
* The endpoint answered with a JSON-RPC error, such as the native server's for invalid arguments
|
|
54
|
+
* or an unknown tool.
|
|
51
55
|
*/
|
|
52
|
-
var
|
|
56
|
+
var ProtocolError = class extends Schema.TaggedError()("ProtocolError", {
|
|
57
|
+
code: Schema.Finite,
|
|
58
|
+
message: Schema.String
|
|
59
|
+
}) {};
|
|
60
|
+
/** The endpoint answered 200 without a JSON-RPC reply to read, as JSON or as an event stream. */
|
|
61
|
+
var NoReply = class extends Schema.TaggedError()("NoReply", { body: Schema.String }) {};
|
|
62
|
+
/** The tool returned an error whose text decodes as none of the action's declared errors. */
|
|
63
|
+
var UndeclaredError = class extends Schema.TaggedError()("UndeclaredError", { text: Schema.String }) {};
|
|
64
|
+
/** The tool returned a result without the structured content its action's success needs. */
|
|
65
|
+
var NoStructuredContent = class extends Schema.TaggedError()("NoStructuredContent", { body: Schema.String }) {};
|
|
66
|
+
const Reason = Schema.Union([
|
|
67
|
+
StatusError,
|
|
68
|
+
ProtocolError,
|
|
69
|
+
NoReply,
|
|
70
|
+
UndeclaredError,
|
|
71
|
+
NoStructuredContent
|
|
72
|
+
]);
|
|
73
|
+
const sentenceOf = Match.typeTags()({
|
|
74
|
+
StatusError: ({ status, body }) => `answered ${status}: ${body}`,
|
|
75
|
+
ProtocolError: ({ code, message }) => `failed with ${code}: ${message}`,
|
|
76
|
+
NoReply: ({ body }) => `had no reply: ${body}`,
|
|
77
|
+
UndeclaredError: ({ text }) => `returned an error: ${text}`,
|
|
78
|
+
NoStructuredContent: ({ body }) => `returned no structured content: ${body}`
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* An answer a client method cannot read as the action's success or a declared error: `reason`
|
|
82
|
+
* says which, with what the endpoint sent, so a caller branches on it with
|
|
83
|
+
* `Effect.catchReason("McpCallError", "StatusError", ...)`. `message` names the tool and the
|
|
84
|
+
* reason. Structured content that does not decode as the success is a `SchemaError` instead,
|
|
85
|
+
* and a request that fails an `HttpClientError`.
|
|
86
|
+
*/
|
|
87
|
+
var McpCallError = class extends Schema.TaggedError()("McpCallError", {
|
|
88
|
+
tool: Schema.String,
|
|
89
|
+
reason: Reason,
|
|
90
|
+
message: Schema.String
|
|
91
|
+
}) {};
|
|
92
|
+
const callError = (tool, reason) => new McpCallError({
|
|
93
|
+
tool,
|
|
94
|
+
reason,
|
|
95
|
+
message: `MCP tools/call "${tool}" ${sentenceOf(reason)}`
|
|
96
|
+
});
|
|
53
97
|
const textOf = (block) => block?.type === "text" ? block.text : void 0;
|
|
54
98
|
const ToolCallReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
|
|
55
99
|
code: Schema.Finite,
|
|
@@ -67,7 +111,6 @@ const replyOf = (response, text) => {
|
|
|
67
111
|
const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
|
|
68
112
|
const callTool = (client, url, action, lift, refusedWith, input) => {
|
|
69
113
|
const { name } = action;
|
|
70
|
-
const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
|
|
71
114
|
return Effect.gen(function* () {
|
|
72
115
|
const encoded = yield* Schema.encodeEffect(Schema.toCodecJson(action.input))(input);
|
|
73
116
|
const response = yield* client.execute(request("tools/call", {
|
|
@@ -75,26 +118,26 @@ const callTool = (client, url, action, lift, refusedWith, input) => {
|
|
|
75
118
|
arguments: encoded
|
|
76
119
|
}, { url }));
|
|
77
120
|
const text = yield* response.text;
|
|
78
|
-
if (response.status !== 200) return yield* failWith(refusedWith, text,
|
|
121
|
+
if (response.status !== 200) return yield* failWith(refusedWith, text, callError(name, new StatusError({
|
|
122
|
+
status: response.status,
|
|
123
|
+
body: text
|
|
124
|
+
})));
|
|
79
125
|
const reply = replyOf(response, text);
|
|
80
|
-
if (Option.isNone(reply)) return yield* Effect.fail(
|
|
81
|
-
if ("error" in reply.value)
|
|
82
|
-
const { code, message } = reply.value.error;
|
|
83
|
-
return yield* Effect.fail(other(`failed with ${code}: ${message}`));
|
|
84
|
-
}
|
|
126
|
+
if (Option.isNone(reply)) return yield* Effect.fail(callError(name, new NoReply({ body: text })));
|
|
127
|
+
if ("error" in reply.value) return yield* Effect.fail(callError(name, new ProtocolError(reply.value.error)));
|
|
85
128
|
const { result } = reply.value;
|
|
86
129
|
if (result.isError === true) {
|
|
87
130
|
const error = textOf(result.content.find((block) => block.type === "text")) ?? "";
|
|
88
|
-
return yield* failWith(projectedErrors(action), error,
|
|
131
|
+
return yield* failWith(projectedErrors(action), error, callError(name, new UndeclaredError({ text: error })));
|
|
89
132
|
}
|
|
90
|
-
if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(
|
|
133
|
+
if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(callError(name, new NoStructuredContent({ body: text })));
|
|
91
134
|
return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(unliftedFrom(lift, result));
|
|
92
135
|
});
|
|
93
136
|
};
|
|
94
|
-
function client(actions, { url = defaultPath, transformClient
|
|
137
|
+
function client(actions, { url = defaultPath, transformClient, authentication } = {}) {
|
|
95
138
|
assertOnce("action", actions);
|
|
96
139
|
return Effect.map(HttpClient.HttpClient, (native) => {
|
|
97
|
-
const http = transformClient(native);
|
|
140
|
+
const http = transformClient === void 0 ? native : transformClient(native);
|
|
98
141
|
return Object.fromEntries(actions.map((action) => {
|
|
99
142
|
const lift = liftOf(action.success);
|
|
100
143
|
const refusedWith = [...Refusal.members, ...action.caller === Anyone ? [] : authentication?.error ?? []];
|
package/dist/mcp/protocol.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unsuspended } from "../contract/
|
|
1
|
+
import { unsuspended } from "../contract/ast.mjs";
|
|
2
2
|
import { Media, isMarked, kindOf } from "../contract/media.mjs";
|
|
3
3
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
4
4
|
import { McpProtocol } from "effect/ai";
|
package/docs/Action.md
CHANGED
|
@@ -158,11 +158,25 @@ and the whole builder above [notes-service/src/notes/handlers.ts](../examples/no
|
|
|
158
158
|
- **Pass the builder to `implement` itself.** Through `.pipe(...)` or a wrapper such as
|
|
159
159
|
`Effect.withSpan(builder, name)`, an `Effect.fn` handler it returns takes an `any` input. A
|
|
160
160
|
builder written apart returns its record `satisfies Action.Handlers<typeof actions>`.
|
|
161
|
-
- **Leave a handler's `Effect.fn` unnamed
|
|
162
|
-
|
|
161
|
+
- **Leave a handler's `Effect.fn` unnamed** unless you need handler-only timing: each action
|
|
162
|
+
already has a span ([Observe calls](#observe-calls)).
|
|
163
163
|
- **Calling the function you passed to `implement` skips authorization.** Call the
|
|
164
164
|
implementation through [`Action.client`](Action-in-process.md#call-actions-in-process).
|
|
165
165
|
|
|
166
|
+
## Observe calls
|
|
167
|
+
|
|
168
|
+
The span named after an action includes authorization, handler execution and call cleanup.
|
|
169
|
+
Its exit reflects the final dispatch outcome; the span and its logs carry `action.name` and
|
|
170
|
+
`action.read_only`.
|
|
171
|
+
|
|
172
|
+
Transport credential verification, input decoding and response encoding run outside this span.
|
|
173
|
+
A request rejected by verification or decoding has no action span; response encoding can fail
|
|
174
|
+
after the action span succeeds.
|
|
175
|
+
|
|
176
|
+
Add a named handler span to measure handler time alone. The named authorizer in
|
|
177
|
+
[notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts)
|
|
178
|
+
similarly measures authorization as a child of the action span.
|
|
179
|
+
|
|
166
180
|
## Authorize calls
|
|
167
181
|
|
|
168
182
|
An implementation holding a protected action states `{ authorize }`: a function of the called
|
|
@@ -233,5 +247,5 @@ failure compiles. What each surface answers is in [guarantees.md](guarantees.md#
|
|
|
233
247
|
or `Forbidden`. Passed on, it is this server's own refusal: a caller whose token is valid is
|
|
234
248
|
told to sign in again. Map it to an error the action declares, or to a defect.
|
|
235
249
|
- **Map that client's `HttpClientError` and `SchemaError` to one declared error a caller can
|
|
236
|
-
act on** ([mcp-tools/src/
|
|
250
|
+
act on** ([mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts)). As
|
|
237
251
|
defects, a model reads only a generic text.
|
|
@@ -63,7 +63,10 @@ The tasks are in [ActionCli.md](ActionCli.md).
|
|
|
63
63
|
`client`. To choose by a condition, build a command from each:
|
|
64
64
|
`remotely ? ActionCli.remote(Http, options) : ActionCli.make(users, options)`.
|
|
65
65
|
- `Type 'HttpClient' is not assignable to type 'never'` in the pipe that runs a remote command:
|
|
66
|
-
provide `NodeHttpClient.layerUndici` (or `FetchHttpClient.layer`).
|
|
66
|
+
provide `NodeHttpClient.layerUndici` (or `FetchHttpClient.layer`). Another service named there
|
|
67
|
+
is what its `client` requires: provide it too.
|
|
68
|
+
- An argument of type `Binding<…>` not assignable at `remote` or `remoteCommand`, beside a
|
|
69
|
+
`client`: the client is another binding's. Make it with `ActionHttp.client` of this one.
|
|
67
70
|
- A local command requires an identity service no handler yields: its action is protected, and
|
|
68
71
|
requires the identity its contract declares. Provide it on the command
|
|
69
72
|
([Name the caller](ActionCli.md#name-the-caller)); do not make the contract public to satisfy
|
|
@@ -79,20 +82,18 @@ The tasks are in [ActionCli.md](ActionCli.md).
|
|
|
79
82
|
- `--help` or a mistyped flag connects to a database, or fails reading config: services provided
|
|
80
83
|
around `Command.run` are built before parsing. Provide them on the command,
|
|
81
84
|
`Command.provide(layer)`.
|
|
82
|
-
- `
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
there. Where every command needs it, keep it required and map the failure, in the Effect that
|
|
87
|
-
provides the client, to `new CliError.UserError({ cause, userMessage })`:
|
|
85
|
+
- `ConfigError: …` or another error's `Tag: message` on stderr from a remote command, a public
|
|
86
|
+
one too: its `client` failed, as when a required token is not set. Read an optional one with
|
|
87
|
+
`Config.option` and add the header only when it is there. Where every command needs it, map
|
|
88
|
+
the failure to a tagged error whose `message` says what to do:
|
|
88
89
|
[notes-service/src/commands.ts](../examples/notes-service/src/commands.ts).
|
|
89
90
|
- `HttpClientError` whose `reason._tag` is `InvalidUrlError`, from a remote command: its client
|
|
90
91
|
has no `baseUrl` and prepends no URL.
|
|
91
92
|
- 401 `Unauthenticated` from a remote command: a remote command adds no headers of its own. Add
|
|
92
|
-
credentials in
|
|
93
|
+
credentials in its client's `transformClient`.
|
|
93
94
|
- Another command's request goes to the API's URL, or carries its token: an `HttpClient`
|
|
94
95
|
configured around the whole program rewrites every request the program makes. Configure it in
|
|
95
|
-
the `client` option
|
|
96
|
+
the `client` option.
|
|
96
97
|
- A report with a stack on stdout, `ERROR (#1): ...`, instead of JSON on stderr: a defect, or a
|
|
97
98
|
failure of a layer the host provides, which `runMain` reports. Fix the defect; apply
|
|
98
99
|
`ActionCli.logToStderr` to keep such reports off stdout.
|
package/docs/ActionCli.md
CHANGED
|
@@ -36,10 +36,10 @@ Import `@gjermundgaraba/effect-actions/ActionCli`.
|
|
|
36
36
|
| `make`, `remote`: `actions` | The actions that are subcommands; default: every one. |
|
|
37
37
|
| `make`, `remote`: `commands` | Each subcommand's `command` options, by action name: `{ getNote: { render } }`. |
|
|
38
38
|
| `make`, `remote`: `subcommands` | Commands of your own beside the derived ones: `[exportNotes]`. |
|
|
39
|
-
| `remoteCommand`, `remote`: `client` | The `ActionHttp.client
|
|
39
|
+
| `remoteCommand`, `remote`: `client` | The client its commands call: `ActionHttp.client(Http, { baseUrl })`. |
|
|
40
40
|
|
|
41
|
-
Exported types: `Options<A
|
|
42
|
-
and `UserError<E>`, a type only: what a command fails with when its action fails with `E`.
|
|
41
|
+
Exported types: `Options<A>` of `make` and `remote`, `CommandOptions<A>` of `command` and
|
|
42
|
+
`remoteCommand`, and `UserError<E>`, a type only: what a command fails with when its action fails with `E`.
|
|
43
43
|
|
|
44
44
|
## Run actions as local commands
|
|
45
45
|
|
|
@@ -47,7 +47,7 @@ Pass implementations to `make`, and provide the services and the caller on the c
|
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
49
|
const assistant = ActionCli.make([tasks, identity], { name: "assistant" }).pipe(
|
|
50
|
-
Command.provide(Layer.mergeAll(
|
|
50
|
+
Command.provide(Layer.mergeAll(tasksFile, Permissions.layerMemory)),
|
|
51
51
|
Command.provideSync(CurrentCaller, operator),
|
|
52
52
|
);
|
|
53
53
|
```
|
|
@@ -81,21 +81,22 @@ Run it with Effect's own `Command.run`:
|
|
|
81
81
|
Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler; each
|
|
82
82
|
command calls its action through the binding's `ActionHttp.client` method
|
|
83
83
|
([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)) and fails as that method does. The host
|
|
84
|
-
provides `HttpClient` (`NodeHttpClient.layerUndici`)
|
|
85
|
-
[notes-service/src/
|
|
86
|
-
[notes-service/src/
|
|
84
|
+
provides `HttpClient` (`NodeHttpClient.layerUndici`) in
|
|
85
|
+
[notes-service/src/cli.ts](../examples/notes-service/src/cli.ts); the client says where the
|
|
86
|
+
server is in [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts).
|
|
87
87
|
[kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header and uses
|
|
88
88
|
every command option.
|
|
89
89
|
|
|
90
|
-
- **
|
|
91
|
-
`client: { baseUrl, transformClient
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
- **The connection is the `client` option**, an Effect of the binding's client run each time a
|
|
91
|
+
command runs: `client: ActionHttp.client(Http, { baseUrl, transformClient })`. One reading
|
|
92
|
+
`Config` is an `Effect.gen` returning `yield* ActionHttp.client(…)`, so `--help` reads nothing.
|
|
93
|
+
Its failures and its methods' are the command's, printed as `Tag: message`, a
|
|
94
|
+
`CliError.UserError` as its `userMessage`; what they require, the host provides. Credentials go in `transformClient`
|
|
95
|
+
([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)).
|
|
96
|
+
- **Never transform the `HttpClient` around the program**: it rewrites every request the program
|
|
97
|
+
makes, an absolute URL too, and sends each one the token.
|
|
98
|
+
- **A token every command needs is required, its failure mapped to a tagged error whose `message`
|
|
99
|
+
says what to do**, or a missing one prints as `ConfigError: …`: `api` in
|
|
99
100
|
[notes-service/src/commands.ts](../examples/notes-service/src/commands.ts). Where a public
|
|
100
101
|
command must answer signed out, read it with `Config.option` and add the header when it is
|
|
101
102
|
there: [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts).
|
|
@@ -156,15 +157,15 @@ in `subcommands`: `notes export <file>` takes `Argument.String("file")`
|
|
|
156
157
|
([notes-service/src/commands.ts](../examples/notes-service/src/commands.ts)):
|
|
157
158
|
|
|
158
159
|
```ts
|
|
159
|
-
const notes = ActionCli.remote(Http, { name: "notes", subcommands: [exportNotes] });
|
|
160
|
+
const notes = ActionCli.remote(Http, { name: "notes", client: api, subcommands: [exportNotes] });
|
|
160
161
|
```
|
|
161
162
|
|
|
162
163
|
- **Never pipe `Command.withSubcommands` onto `make` or `remote`.** It replaces the derived
|
|
163
164
|
commands, which then answer with the usage text.
|
|
164
|
-
- **Call the actions through a client**, in the command's own handler: `
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
`authorize` still runs.
|
|
165
|
+
- **Call the actions through a client**, in the command's own handler: `yield* api`, the Effect
|
|
166
|
+
the aggregate's `client` is, which needs no scope, or `Action.client`, acquired inside
|
|
167
|
+
`Effect.scoped` ([Action-in-process.md](Action-in-process.md#call-actions-in-process)).
|
|
168
|
+
`authorize` still runs.
|
|
168
169
|
- **Its output is what it prints**, with `Console.log`: the output rules below are a derived
|
|
169
170
|
command's. **Pipe its whole Effect through `ActionCli.toUserError(Http)`**, the binding its
|
|
170
171
|
client is made from, HTTP or RPC, or `toUserError([GetNote])` over `Action.client`, to fail as a derived command does: a declared
|
|
@@ -53,6 +53,12 @@ The tasks are in [ActionHttp.md](ActionHttp.md).
|
|
|
53
53
|
the binding's `error`, given inline so its type is certain, or move the check to the handlers
|
|
54
54
|
and declare the error on their actions.
|
|
55
55
|
|
|
56
|
+
- `No overload matches this call` at `client`, naming `Options<unknown, unknown>`: options built
|
|
57
|
+
before the call hold the generic `HttpClient.mapRequestEffect(authorize)`, whose failures the
|
|
58
|
+
call cannot infer, or a helper types them as `Parameters<typeof ActionHttp.client>[1]`. Write
|
|
59
|
+
the transform as `(client) => HttpClient.mapRequestEffect(client, authorize)`, and type such
|
|
60
|
+
options as `ActionHttp.ClientOptions<E, R>`.
|
|
61
|
+
|
|
56
62
|
## Requests
|
|
57
63
|
|
|
58
64
|
- A route answers without credentials: its contract is `caller: Action.Anyone`.
|
package/docs/ActionHttp.md
CHANGED
|
@@ -36,7 +36,7 @@ Exported types: `Binding`; `Any`, any binding; `Client<typeof Http>`, a client's
|
|
|
36
36
|
| `layer`: `actions` | The bound actions it serves: `[GetUser]`. Defaults to every one the implementations hold. |
|
|
37
37
|
| `layer`: `middleware` | Native `HttpApiMiddleware` around every route it serves, the first innermost. |
|
|
38
38
|
| `client`: `baseUrl` | What routes resolve against: `https://api.example.com`. Omitted: relative ones, a page's origin. |
|
|
39
|
-
| `client`: `transformClient` | Wraps the native `HttpClient`:
|
|
39
|
+
| `client`: `transformClient` | Wraps the native `HttpClient`: a token, or a sign-in per request, whose failures each call adds. |
|
|
40
40
|
| `fetchClient`: `fetch` | Its `fetch`, beside `client`'s options. Omitted: the global `fetch`, looked up on every call. |
|
|
41
41
|
|
|
42
42
|
Each action is served at `POST <prefix>/<action>`, a `readOnly` one too. One without `success`
|
|
@@ -185,9 +185,12 @@ Files: [quickstart/src/client.ts](../examples/quickstart/src/client.ts),
|
|
|
185
185
|
[kitchen-sink/src/seed.ts](../examples/kitchen-sink/src/seed.ts) (`baseUrl` from `Config`,
|
|
186
186
|
headers, errors by tag).
|
|
187
187
|
|
|
188
|
-
- Outside a browser, set `baseUrl`. A token or any other header goes through `transformClient
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
- Outside a browser, set `baseUrl`. A token or any other header goes through `transformClient`:
|
|
189
|
+
`HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. A credential read or refreshed
|
|
190
|
+
as each request is sent, which may fail, is
|
|
191
|
+
`(client) => HttpClient.mapRequestEffect(client, authorize)`: each call then also fails with
|
|
192
|
+
`authorize`'s own errors, as they are, and requires its services. `ActionMcp.client` takes the
|
|
193
|
+
same transform, and `ActionCli.remote` the client itself as its `client`.
|
|
191
194
|
- Leave the argument out when `{}` is a valid input. A given argument is the decoded input, so
|
|
192
195
|
it states a defaulted field.
|
|
193
196
|
- A declared error is a typed failure, as its decoded value: the action's own, the binding's,
|
|
@@ -215,6 +218,8 @@ with `Effect.runPromise`. Files:
|
|
|
215
218
|
- A page on another origin passes `baseUrl`, and the server must allow that origin with
|
|
216
219
|
`HttpRouter.cors`
|
|
217
220
|
([kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts), `cors`).
|
|
221
|
+
- A `transformClient` requiring a service, such as the session's token, makes each call require
|
|
222
|
+
it: provide it on the call before `Effect.runPromise`.
|
|
218
223
|
- An unhandled declared error rejects the promise as its decoded value, so
|
|
219
224
|
`error instanceof Action.Unauthenticated` works in a `catch`. Handle an expected one by its tag
|
|
220
225
|
before `Effect.runPromise`.
|
package/docs/ActionMcp.md
CHANGED
|
@@ -21,7 +21,7 @@ Import `@gjermundgaraba/effect-actions/ActionMcp`.
|
|
|
21
21
|
| `LayerHttpOptions` | Type of `layerHttp`'s options: `Options` and HTTP's own. |
|
|
22
22
|
| `client(actions, options?)` | An Effect of a client calling each action's tool. |
|
|
23
23
|
| `request(method, params?, options?)` | One MCP request, as the native `HttpClientRequest` to send. |
|
|
24
|
-
| `McpCallError` | A client method's
|
|
24
|
+
| `McpCallError` | A client method's undecodable answer, by tagged `reason`. |
|
|
25
25
|
|
|
26
26
|
`implementations` is one implementation or a list.
|
|
27
27
|
|
|
@@ -44,7 +44,7 @@ options. Its `protocols` and `allowSessionTermination` are not options.
|
|
|
44
44
|
|
|
45
45
|
`client` takes `url`, default `/mcp`, `transformClient`, and the endpoint's `authentication`, so an
|
|
46
46
|
error its verifier declares decodes; `request` takes `url` and `headers`.
|
|
47
|
-
Types: `Client<Actions, D>`, `ClientOptions`, `RequestOptions`, `Params`.
|
|
47
|
+
Types: `Client<Actions, D, E, R>`, `ClientOptions`, `RequestOptions`, `Params`.
|
|
48
48
|
|
|
49
49
|
## Serve tools over HTTP
|
|
50
50
|
|
|
@@ -140,7 +140,7 @@ tool's read-only hint, and the contract's `mcp` option holds its `title`, its ot
|
|
|
140
140
|
field's `description`, annotated before the check: after a custom one it is lost too.
|
|
141
141
|
- A declared error reaches the model as an `isError` result whose text is the error's JSON. Write
|
|
142
142
|
its `message` for the model, which acts on it:
|
|
143
|
-
[mcp-tools/src/
|
|
143
|
+
[mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts). Every other case:
|
|
144
144
|
[guarantees.md](guarantees.md#wire-behavior).
|
|
145
145
|
- `actions` lists the tools, and only listed actions must suit MCP:
|
|
146
146
|
[kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts). Leave out an action a
|