@gjermundgaraba/effect-actions 0.1.0-rc.1 → 0.1.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 +50 -219
- package/dist/Action.d.mts +62 -2
- package/dist/Action.mjs +7 -4
- package/dist/ActionCatalog.d.mts +31 -0
- package/dist/ActionCatalog.mjs +36 -0
- package/dist/ActionCli.d.mts +27 -0
- package/dist/ActionCli.mjs +26 -0
- package/dist/ActionCliClient.d.mts +45 -0
- package/dist/ActionCliClient.mjs +52 -0
- package/dist/ActionGroup.d.mts +15 -14
- package/dist/ActionGroup.mjs +3 -2
- package/dist/ActionHttp.d.mts +21 -34
- package/dist/ActionHttp.mjs +44 -51
- package/dist/ActionMcp.d.mts +28 -7
- package/dist/ActionMcp.mjs +20 -128
- package/dist/ActionToolkit.d.mts +37 -0
- package/dist/ActionToolkit.mjs +10 -0
- package/dist/Authentication.d.mts +11 -12
- package/dist/Authentication.mjs +24 -57
- package/dist/Testing.d.mts +7 -4
- package/dist/Testing.mjs +4 -2
- package/dist/TestingClient.d.mts +6 -3
- package/dist/TestingClient.mjs +2 -2
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/internal/actions.d.mts +18 -0
- package/dist/internal/actions.mjs +16 -0
- package/dist/internal/cli.d.mts +29 -0
- package/dist/internal/cli.mjs +36 -0
- package/dist/internal/implementation.d.mts +39 -0
- package/dist/internal/implementation.mjs +37 -0
- package/dist/internal/tools.mjs +84 -0
- package/docs/Action.md +117 -0
- package/docs/ActionCatalog.md +56 -0
- package/docs/ActionCli.md +91 -0
- package/docs/ActionCliClient.md +77 -0
- package/docs/ActionGroup.md +141 -0
- package/docs/ActionHttp.md +106 -0
- package/docs/ActionMcp.md +131 -0
- package/docs/ActionToolkit.md +73 -0
- package/docs/Authentication.md +114 -0
- package/docs/CONTEXT.md +36 -0
- package/docs/README.md +85 -0
- package/docs/Testing.md +91 -0
- package/docs/guarantees.md +55 -0
- package/docs/setup.md +63 -0
- package/package.json +19 -14
- package/dist/Action-CXsfTEhx.d.mts +0 -54
- package/dist/implementation-4Q2BESw5.mjs +0 -61
- package/dist/implementation-DTqp_0DW.d.mts +0 -61
package/dist/ActionGroup.d.mts
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Action, Action_d_exports, Any as Any$1, Codec, Handler } from "./Action.mjs";
|
|
2
|
+
import { Actions, SchemaErrorPolicy } from "./internal/actions.mjs";
|
|
3
|
+
import { Implementation } from "./internal/implementation.mjs";
|
|
3
4
|
import { Effect, Scope } from "effect";
|
|
4
|
-
|
|
5
|
+
declare namespace ActionGroup_d_exports {
|
|
6
|
+
export { Any, Group, Implementation, Options, SchemaErrorPolicy, make };
|
|
7
|
+
}
|
|
5
8
|
type HandlersFrom<Actions extends ReadonlyArray<Any$1>> = { readonly [A in Actions[number] as A["name"]]: Handler<A, any>; };
|
|
6
|
-
type HandlersContext<H> = { [K in keyof H]: H[K] extends ((input: never) => Effect.Effect<infer _A, infer _E, infer R>) ? R : never; }[keyof H];
|
|
7
9
|
/** Group-level errors join each action's own, so a shared set is declared once. */
|
|
8
|
-
type WithErrors<Actions extends ReadonlyArray<Any$1>, Errors extends ReadonlyArray<Codec>> = { readonly [K in keyof Actions]: Actions[K] extends Action<infer Name, infer Input, infer Output, infer Own, infer Http> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Http> : never; };
|
|
10
|
+
type WithErrors<Actions extends ReadonlyArray<Any$1>, Errors extends ReadonlyArray<Codec>> = { readonly [K in keyof Actions]: Actions[K] extends Action<infer Name, infer Input, infer Output, infer Own, infer Http, infer Mcp> ? Action<Name, Input, Output, readonly [...Own, ...Errors], Http, Mcp> : never; };
|
|
11
|
+
/** What `make` needs to define a group. */
|
|
9
12
|
export interface Options<Name extends string, Errors extends ReadonlyArray<Codec>, PolicyErrors extends ReadonlyArray<Codec>> {
|
|
10
13
|
/** The `HttpApiGroup` identifier, and so the OpenAPI tag and operation-ID prefix. */
|
|
11
14
|
readonly name: Name;
|
|
12
15
|
/** Failures every action of the group may have, added to each action's own. */
|
|
13
16
|
readonly errors?: Errors;
|
|
14
|
-
/** How
|
|
17
|
+
/** How HTTP answers failed decoding or encoding of these actions; MCP keeps its native answers. */
|
|
15
18
|
readonly schemaError?: SchemaErrorPolicy<PolicyErrors>;
|
|
16
19
|
}
|
|
20
|
+
/** A named set of action contracts: what adapters serve and what `implement` binds. */
|
|
17
21
|
export interface Group<Name extends string, Actions$1 extends ReadonlyArray<Any$1>, PolicyErrors extends ReadonlyArray<Codec> = ReadonlyArray<Codec>> extends Actions<Name, Actions$1, PolicyErrors> {
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
* once (`const users = yield* Users`); services yielded inside a handler are
|
|
21
|
-
* request-scoped instead. Scoped acquisition lasts for the adapter runtime.
|
|
22
|
-
*/
|
|
23
|
-
readonly implement: <H extends HandlersFrom<Actions$1>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Group<Name, Actions$1, PolicyErrors>, HandlersContext<H>, NoInfer<EX>, NoInfer<Exclude<RX, Scope.Scope>>>;
|
|
22
|
+
/** Bind every handler at once, resolving build-time services once per adapter layer. */
|
|
23
|
+
readonly implement: <H extends HandlersFrom<Actions$1>, EX = never, RX = never>(build: H | Effect.Effect<H, EX, RX>) => Implementation<Group<Name, Actions$1, PolicyErrors>, H, NoInfer<EX>, NoInfer<Exclude<RX, Scope.Scope>>>;
|
|
24
24
|
}
|
|
25
|
+
/** Any group, with its actions erased. */
|
|
25
26
|
export type Any = Group<string, ReadonlyArray<Any$1>>;
|
|
26
|
-
/**
|
|
27
|
+
/** Define a group. Invalid or duplicate action and MCP names fail here, at definition time. */
|
|
27
28
|
export declare function make<const Name extends string, const Actions extends ReadonlyArray<Any$1>, const Errors extends ReadonlyArray<Codec> = [], const PolicyErrors extends ReadonlyArray<Codec> = []>(options: Options<Name, Errors, PolicyErrors>, ...actions: Actions): Group<Name, WithErrors<Actions, Errors>, PolicyErrors>;
|
|
28
29
|
//#endregion
|
|
29
|
-
export type
|
|
30
|
+
export { ActionGroup_d_exports, type Implementation, type SchemaErrorPolicy };
|
package/dist/ActionGroup.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertDistinct, assertName } from "./internal/actions.mjs";
|
|
2
|
+
import { Implementation } from "./internal/implementation.mjs";
|
|
2
3
|
import { Effect } from "effect";
|
|
3
4
|
//#region src/ActionGroup.ts
|
|
4
5
|
function make(options, ...declared) {
|
|
5
6
|
const { name } = options;
|
|
6
|
-
|
|
7
|
+
assertName("action group name", name);
|
|
7
8
|
const actions = declared.map((action) => ({
|
|
8
9
|
...action,
|
|
9
10
|
errors: [...action.errors, ...options.errors ?? []]
|
package/dist/ActionHttp.d.mts
CHANGED
|
@@ -1,52 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Action_d_exports, Any, Codec } from "./Action.mjs";
|
|
2
|
+
import { Actions, PolicyError } from "./internal/actions.mjs";
|
|
3
|
+
import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
|
|
4
|
+
import { Layer, Schema } from "effect";
|
|
5
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
|
6
|
+
import { Etag, HttpPlatform, HttpRouter } from "effect/unstable/http";
|
|
6
7
|
import { FileSystem } from "effect/FileSystem";
|
|
7
8
|
import { Path } from "effect/Path";
|
|
8
9
|
declare namespace ActionHttp_d_exports {
|
|
9
|
-
export { Api,
|
|
10
|
+
export { Api, Http, Options, make };
|
|
10
11
|
}
|
|
11
12
|
/** Contract-level configuration: servers and clients must agree on it. */
|
|
12
13
|
export interface Options {
|
|
13
14
|
readonly apiPath: `/${string}`;
|
|
14
15
|
}
|
|
15
|
-
/** Distribute over the tuple so each name retains its own codecs. */
|
|
16
16
|
type Endpoint<A extends Any, E extends Codec> = A extends {
|
|
17
17
|
readonly http: false;
|
|
18
|
-
} ? never : A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["errors"][number] | E
|
|
19
|
-
/** One native group per action group; groups without HTTP actions are omitted. */
|
|
18
|
+
} ? never : A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["errors"][number] | E>, never> : never;
|
|
20
19
|
type ApiGroup<G extends Actions> = G extends Actions ? [Endpoint<G["actions"][number], never>] extends [never] ? never : HttpApiGroup.HttpApiGroup<G["name"], Endpoint<G["actions"][number], PolicyError<G>>> : never;
|
|
20
|
+
/** The native `HttpApi` of every HTTP-enabled action of `G`, one group per action group. */
|
|
21
21
|
export type Api<G extends Actions> = HttpApi.HttpApi<"actions", ApiGroup<G>>;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export type Client<G extends Actions> = { readonly [Pair in Served<G> as Pair["action"]["http"] extends false ? never : Pair["action"]["name"]]: (...args: {} extends Pair["action"]["input"]["Type"] ? [input?: Pair["action"]["input"]["Type"]] : [input: Pair["action"]["input"]["Type"]]) => HttpApiClient.Client.MethodReturn<Endpoint<Pair["action"], Pair["policyError"]>, never, never, "decoded-only">; };
|
|
28
|
-
/** Native connection options: `baseUrl`, `transformClient`, `transformResponse`. */
|
|
29
|
-
export type ClientOptions = NonNullable<Parameters<typeof HttpApiClient.make>[1]>;
|
|
30
|
-
export interface Http<G extends Actions> {
|
|
22
|
+
/** A namespaced HTTP binding. */
|
|
23
|
+
export interface Http<Groups extends ReadonlyArray<Actions>> {
|
|
24
|
+
/** The exact contracts bound to this HTTP adapter, in declaration order. */
|
|
25
|
+
readonly groups: Groups;
|
|
26
|
+
readonly api: Api<Groups[number]>;
|
|
31
27
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* Serve any number of implementations in one layer. Build failures, build
|
|
29
|
+
* services and request services are unions over precisely those apps.
|
|
34
30
|
*/
|
|
35
|
-
readonly
|
|
36
|
-
/**
|
|
37
|
-
* The routes of one group. Merge one per group; router middleware provided
|
|
38
|
-
* to a layer applies to that group only. Handler requirements are
|
|
39
|
-
* request-level requirements, exactly as native `HttpApiBuilder` handlers'
|
|
40
|
-
* are. A group without HTTP actions registers nothing and is not acquired.
|
|
41
|
-
*/
|
|
42
|
-
readonly layer: <App extends AnyImplementation<G>>(app: App) => Layer.Layer<never, BuildError<App>, BuildContext<App> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<App>> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
|
|
43
|
-
/** Direct action methods backed by the native HTTP client and its codecs. */
|
|
44
|
-
readonly client: (options?: ClientOptions) => Effect.Effect<Client<G>, never, HttpClient.HttpClient>;
|
|
31
|
+
readonly layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>>(...apps: Apps) => Layer.Layer<never, BuildError<Apps[number]>, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
|
|
45
32
|
}
|
|
46
33
|
/**
|
|
47
|
-
* Bind the contract-level configuration once.
|
|
48
|
-
*
|
|
34
|
+
* Bind the contract-level configuration once. Every route is `POST
|
|
35
|
+
* <apiPath>/<group>/<action>`, so action names may repeat across groups.
|
|
49
36
|
*/
|
|
50
|
-
export declare function make<const G extends ReadonlyArray<Actions>>(options: Options, ...groups: G): Http<G
|
|
37
|
+
export declare function make<const G extends ReadonlyArray<Actions>>(options: Options, ...groups: G): Http<G>;
|
|
51
38
|
//#endregion
|
|
52
|
-
export { ActionHttp_d_exports
|
|
39
|
+
export { ActionHttp_d_exports };
|
package/dist/ActionHttp.mjs
CHANGED
|
@@ -1,73 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { assertDistinct } from "./internal/actions.mjs";
|
|
2
|
+
import { dispatch } from "./internal/implementation.mjs";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi";
|
|
4
5
|
//#region src/ActionHttp.ts
|
|
5
|
-
const
|
|
6
|
+
const httpActions = (group) => group.actions.filter((action) => action.http);
|
|
7
|
+
const endpoint = (apiPath, group, action) => HttpApiEndpoint.post(action.name, `${apiPath}/${group.name}/${action.name}`, {
|
|
6
8
|
payload: action.input,
|
|
7
9
|
success: action.success,
|
|
8
|
-
error: [...action.errors, ...errors]
|
|
10
|
+
error: [...action.errors, ...group.schemaError?.errors ?? []]
|
|
9
11
|
}).annotate(OpenApi.Description, action.description);
|
|
10
|
-
function erasedApi(
|
|
11
|
-
const [first, ...rest] =
|
|
12
|
-
const [
|
|
13
|
-
return
|
|
12
|
+
function erasedApi(groups, options) {
|
|
13
|
+
const [first, ...rest] = groups.flatMap((group) => {
|
|
14
|
+
const [first, ...rest] = httpActions(group).map((action) => endpoint(options.apiPath, group, action));
|
|
15
|
+
return first === void 0 ? [] : [HttpApiGroup.make(group.name).add(first, ...rest)];
|
|
14
16
|
});
|
|
15
17
|
const empty = HttpApi.make("actions");
|
|
16
18
|
return first === void 0 ? empty : empty.add(first, ...rest);
|
|
17
19
|
}
|
|
18
20
|
const schemaErrorMiddleware = (policy) => {
|
|
19
|
-
class SchemaErrors extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: policy
|
|
21
|
+
class SchemaErrors extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: policy.errors }) {}
|
|
20
22
|
return {
|
|
21
23
|
SchemaErrors,
|
|
22
|
-
layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy
|
|
23
|
-
phase: failure.kind === "Body" || failure.kind === "ResponseHeaders" ? "output" : "input",
|
|
24
|
-
cause: failure.cause
|
|
25
|
-
})))
|
|
24
|
+
layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(policy.map(failure)))
|
|
26
25
|
};
|
|
27
26
|
};
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
/**
|
|
28
|
+
* `HttpApiBuilder.handleAll` registers a dynamically assembled record. The
|
|
29
|
+
* action selected by the native router determines the input before `dispatch`
|
|
30
|
+
* calls its handler, so this is the one adapter boundary where exact handlers
|
|
31
|
+
* are viewed as an erased record. `HandlersContext` remains on the dispatch
|
|
32
|
+
* effect and is restored by `Http.layer`'s public signature.
|
|
33
|
+
*/
|
|
34
|
+
const erasedHandlers = (handlers) => {
|
|
35
|
+
return handlers;
|
|
36
|
+
};
|
|
37
|
+
/** Build routes for one group. Handler erasure is limited to native dynamic endpoint registration. */
|
|
38
|
+
const groupLayer = (options, app) => {
|
|
39
|
+
const actions = httpActions(app.group);
|
|
40
|
+
if (actions.length === 0) return Layer.empty;
|
|
41
|
+
const api = erasedApi([app.group], options);
|
|
42
|
+
const entries = (record) => Object.fromEntries(actions.map((action) => [action.name, (request) => dispatch(app.group, action, record)(request.payload)]));
|
|
43
|
+
const handlers = (record) => HttpApiBuilder.group(api, app.group.name, (builder) => builder.handleAll(entries(record)));
|
|
44
|
+
if (app.group.schemaError === void 0) return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.layer(api).pipe(Layer.provide(handlers(erasedHandlers(record))))));
|
|
45
|
+
const schemaErrors = schemaErrorMiddleware(app.group.schemaError);
|
|
46
|
+
const policyApi = api.middleware(schemaErrors.SchemaErrors);
|
|
47
|
+
const policyHandlers = (record) => HttpApiBuilder.group(policyApi, app.group.name, (builder) => {
|
|
48
|
+
const dynamicEntries = entries(record);
|
|
49
|
+
return builder.handleAll(dynamicEntries);
|
|
45
50
|
});
|
|
51
|
+
return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.layer(policyApi).pipe(Layer.provide(policyHandlers(erasedHandlers(record))), Layer.provide(schemaErrors.layer))));
|
|
46
52
|
};
|
|
47
|
-
const erasedClient = (api, views, connection) => Effect.map(HttpApiClient.make(api, connection), (native) => {
|
|
48
|
-
const acceptsUndefined = new Set(views.flatMap(({ actions }) => actions.flatMap((action) => Schema.is(action.input)(void 0) ? [action.name] : [])));
|
|
49
|
-
return Object.fromEntries(Object.values(native).flatMap((methods) => Object.entries(methods).map(([name, method]) => {
|
|
50
|
-
if (name === "then") throw new Error("Action \"then\" requires the native grouped HttpApiClient");
|
|
51
|
-
return [name, (input) => method({
|
|
52
|
-
payload: input === void 0 && !acceptsUndefined.has(name) ? {} : input,
|
|
53
|
-
responseMode: "decoded-only"
|
|
54
|
-
})];
|
|
55
|
-
})));
|
|
56
|
-
});
|
|
57
53
|
function make(options, ...groups) {
|
|
58
|
-
const views = served(groups, (action) => action.http);
|
|
59
54
|
assertDistinct("action group", groups.map((group) => group.name));
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
views,
|
|
65
|
-
options
|
|
55
|
+
const layer = (...apps) => {
|
|
56
|
+
assertDistinct("implementation group", apps.map((app) => app.group.name));
|
|
57
|
+
for (const app of apps) if (!groups.includes(app.group)) throw new Error(`Implementation of group "${app.group.name}" is not served by this adapter`);
|
|
58
|
+
return apps.map((app) => groupLayer(options, app)).reduce((all, app) => Layer.merge(all, app), Layer.empty);
|
|
66
59
|
};
|
|
67
60
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
groups,
|
|
62
|
+
api: erasedApi(groups, options),
|
|
63
|
+
layer
|
|
71
64
|
};
|
|
72
65
|
}
|
|
73
66
|
//#endregion
|
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
|
|
2
2
|
import { Cause, Layer } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { McpServer } from "effect/unstable/ai";
|
|
4
4
|
import { HttpRouter } from "effect/unstable/http";
|
|
5
|
-
import {
|
|
5
|
+
import { Stdio } from "effect/Stdio";
|
|
6
6
|
//#region src/ActionMcp.d.ts
|
|
7
|
+
/** One Streamable HTTP MCP endpoint; its fields become the native server info. */
|
|
7
8
|
export interface Options {
|
|
8
9
|
readonly name: string;
|
|
9
10
|
readonly version: string;
|
|
11
|
+
/** Native protocol adapters to serve; negotiation and sessions are owned by Effect. */
|
|
12
|
+
readonly protocols: Parameters<typeof McpServer.layerHttp>[0]["protocols"];
|
|
13
|
+
/** Route of the Streamable HTTP endpoint; no default. */
|
|
10
14
|
readonly path: HttpRouter.PathInput;
|
|
11
|
-
|
|
15
|
+
/** Browser origins accepted by the native server; passed through to `McpServer.layerHttp`. */
|
|
12
16
|
readonly allowedOrigins?: ReadonlyArray<string>;
|
|
13
17
|
readonly instructions?: string;
|
|
14
18
|
}
|
|
19
|
+
/** Server information for an MCP subprocess connected through standard I/O. */
|
|
20
|
+
export interface StdioOptions {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly version: string;
|
|
23
|
+
/** Native protocol adapters to serve; negotiation is owned by Effect. */
|
|
24
|
+
readonly protocols: Parameters<typeof McpServer.layerStdio>[0]["protocols"];
|
|
25
|
+
readonly instructions?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Serve MCP tools over one Streamable HTTP endpoint.
|
|
29
|
+
*
|
|
30
|
+
* Middleware provided around this layer has the normal HTTP lifetime. Native
|
|
31
|
+
* context capture applies: never provide request-identity tags at startup.
|
|
32
|
+
*/
|
|
33
|
+
export declare function layerHttp<const Apps extends ReadonlyArray<AnyImplementation>>(options: Options, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]>>>;
|
|
15
34
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
35
|
+
* Serve MCP tools through newline-delimited JSON-RPC on standard I/O.
|
|
36
|
+
*
|
|
37
|
+
* The host supplies the `Stdio` service. Arguments are tool input only and
|
|
38
|
+
* never establish request identity or authority.
|
|
18
39
|
*/
|
|
19
|
-
export declare function
|
|
40
|
+
export declare function layerStdio<const Apps extends ReadonlyArray<AnyImplementation>>(options: StdioOptions, ...apps: Apps): Layer.Layer<never, BuildError<Apps[number]> | Cause.IllegalArgumentError, BuildContext<Apps[number]> | Stdio | RequestContext<Apps[number]>>;
|
|
20
41
|
//#endregion
|
package/dist/ActionMcp.mjs
CHANGED
|
@@ -1,133 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import "./internal/implementation.mjs";
|
|
2
|
+
import { bindTools } from "./internal/tools.mjs";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
import { McpServer } from "effect/unstable/ai";
|
|
5
5
|
//#region src/ActionMcp.ts
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
McpProtocol.v2025_11_25,
|
|
10
|
-
McpProtocol.v2025_06_18,
|
|
11
|
-
McpProtocol.v2025_03_26,
|
|
12
|
-
McpProtocol.v2024_11_05
|
|
13
|
-
];
|
|
14
|
-
const toolResult = (structuredContent, isError) => new McpSchema.CallToolResult({
|
|
15
|
-
isError,
|
|
16
|
-
structuredContent,
|
|
17
|
-
content: [{
|
|
18
|
-
type: "text",
|
|
19
|
-
text: JSON.stringify(structuredContent)
|
|
20
|
-
}]
|
|
21
|
-
});
|
|
22
|
-
const resolveReference = (name, what, schema, definitions) => {
|
|
23
|
-
if (!Predicate.isString(schema.$ref)) return schema;
|
|
24
|
-
const reference = schema.$ref;
|
|
25
|
-
const path = JsonPointer.parseUriFragment(reference);
|
|
26
|
-
const key = path?.[1];
|
|
27
|
-
if (path?.length !== 2 || path[0] !== "$defs" || key === void 0) throw new Error(`${name}: unsupported MCP ${what} reference ${reference}`);
|
|
28
|
-
const definition = Object.hasOwn(definitions, key) ? definitions[key] : void 0;
|
|
29
|
-
if (definition === void 0) throw new Error(`${name}: missing MCP ${what} reference ${reference}`);
|
|
30
|
-
return definition;
|
|
6
|
+
const registration = (apps) => {
|
|
7
|
+
const binding = bindTools(apps, "mcp");
|
|
8
|
+
return Layer.effectDiscard(McpServer.registerToolkit(binding.toolkit)).pipe(Layer.provide(binding.layer));
|
|
31
9
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
/** Declared errors are published as-is in `structuredContent`, so each must encode to an object. */
|
|
43
|
-
const assertObjectError = (owner, error) => {
|
|
44
|
-
const { schema, definitions } = Schema.toJsonSchemaDocument(error);
|
|
45
|
-
const visited = /* @__PURE__ */ new Set();
|
|
46
|
-
const encodesObject = (candidate) => {
|
|
47
|
-
const root = resolveReference(owner, "error", candidate, definitions);
|
|
48
|
-
if (root.type === "object" || visited.has(root)) return true;
|
|
49
|
-
visited.add(root);
|
|
50
|
-
const members = root.anyOf ?? root.oneOf;
|
|
51
|
-
return Array.isArray(members) && members.length > 0 && members.every(encodesObject);
|
|
52
|
-
};
|
|
53
|
-
if (!encodesObject(schema)) throw new Error(`${owner}: MCP error must have an object root`);
|
|
54
|
-
};
|
|
55
|
-
const registerTool = (app, action, policy) => Effect.gen(function* () {
|
|
56
|
-
if (action.mcp === false) return;
|
|
57
|
-
const server = yield* McpServer.McpServer;
|
|
58
|
-
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(inputJsonSchema(action)).pipe(Effect.orDie);
|
|
59
|
-
const errors = [...action.errors, ...policy?.errors ?? []];
|
|
60
|
-
for (const error of action.errors) assertObjectError(action.name, error);
|
|
61
|
-
const input = Schema.toCodecJson(action.input);
|
|
62
|
-
const success = Schema.toCodecJson(action.success);
|
|
63
|
-
const failure = Schema.toCodecJson(Schema.Union(errors));
|
|
64
|
-
const handle = app.handle(action);
|
|
65
|
-
const failureResult = (error) => Schema.encodeUnknownEffect(failure)(error).pipe(Effect.orDie, Effect.map((json) => toolResult(json, true)));
|
|
66
|
-
const schemaFailure = (phase, cause) => {
|
|
67
|
-
if (policy !== void 0) return failureResult(policy.map({
|
|
68
|
-
phase,
|
|
69
|
-
cause
|
|
70
|
-
}));
|
|
71
|
-
if (phase === "input") return Effect.fail(new McpSchema.InvalidParams({ message: cause.message }));
|
|
72
|
-
return Effect.die(cause);
|
|
73
|
-
};
|
|
74
|
-
const successResult = (value) => Schema.encodeUnknownEffect(success)(value).pipe(Effect.matchEffect({
|
|
75
|
-
onFailure: (cause) => schemaFailure("output", cause),
|
|
76
|
-
onSuccess: (json) => Effect.succeed(toolResult({ value: json }, false))
|
|
10
|
+
const server = (apps, transport) => registration(apps).pipe(Layer.provide(transport), Layer.fresh);
|
|
11
|
+
function layerHttp(options, ...apps) {
|
|
12
|
+
return server(apps, McpServer.layerHttp({
|
|
13
|
+
name: options.name,
|
|
14
|
+
version: options.version,
|
|
15
|
+
instructions: options.instructions,
|
|
16
|
+
path: options.path,
|
|
17
|
+
protocols: options.protocols,
|
|
18
|
+
allowedOrigins: options.allowedOrigins
|
|
77
19
|
}));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
description: action.description,
|
|
82
|
-
inputSchema,
|
|
83
|
-
outputSchema: Tool.getJsonSchemaFromSchema(Schema.Struct({ value: action.success })),
|
|
84
|
-
annotations: {
|
|
85
|
-
readOnlyHint: action.mcp.readOnly,
|
|
86
|
-
destructiveHint: action.mcp.destructive
|
|
87
|
-
}
|
|
88
|
-
}),
|
|
89
|
-
annotations: Context.empty(),
|
|
90
|
-
handle: (payload) => Schema.decodeUnknownEffect(input)(payload).pipe(Effect.matchEffect({
|
|
91
|
-
onFailure: (cause) => schemaFailure("input", cause),
|
|
92
|
-
onSuccess: (decoded) => handle(decoded).pipe(Effect.matchEffect({
|
|
93
|
-
onSuccess: successResult,
|
|
94
|
-
onFailure: failureResult
|
|
95
|
-
}))
|
|
96
|
-
}))
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
const registrationRouter = (router, context) => HttpRouter.HttpRouter.of({
|
|
100
|
-
...router,
|
|
101
|
-
add: (method, path, handler, options) => router.add(method, path, handler, options).pipe(Effect.provideContext(context)),
|
|
102
|
-
addAll: (routes) => router.addAll(routes).pipe(Effect.provideContext(context)),
|
|
103
|
-
prefixed: (prefix) => registrationRouter(router.prefixed(prefix), context)
|
|
104
|
-
});
|
|
105
|
-
const erasedLayer = (apps, options) => {
|
|
106
|
-
const serving = apps.flatMap((app) => served([app.group], (action) => action.mcp !== false).map(({ actions }) => ({
|
|
107
|
-
app,
|
|
108
|
-
actions
|
|
109
|
-
})));
|
|
110
|
-
assertDistinct("MCP tool", serving.flatMap(({ actions }) => actions.flatMap((action) => action.mcp === false ? [] : [action.mcp.name])));
|
|
111
|
-
return Implementation.register(serving, (bound) => {
|
|
112
|
-
const native = Layer.effectDiscard(Effect.gen(function* () {
|
|
113
|
-
for (const app of bound) for (const error of app.group.schemaError?.errors ?? []) assertObjectError(`Schema-error policy of group ${app.group.name}`, error);
|
|
114
|
-
yield* Effect.forEach(bound.flatMap((app) => app.actions.map((action) => [app, action])), ([app, action]) => registerTool(app, action, app.group.schemaError), { discard: true });
|
|
115
|
-
})).pipe(Layer.provide(McpServer.layerHttp({
|
|
116
|
-
name: options.name,
|
|
117
|
-
version: options.version,
|
|
118
|
-
instructions: options.instructions,
|
|
119
|
-
path: options.path,
|
|
120
|
-
protocols: options.protocols ?? protocols,
|
|
121
|
-
allowedOrigins: options.allowedOrigins
|
|
122
|
-
})), Layer.fresh);
|
|
123
|
-
return Layer.fromBuildMemo((memoMap, scope) => Effect.gen(function* () {
|
|
124
|
-
const router = registrationRouter(yield* HttpRouter.HttpRouter, yield* Effect.context());
|
|
125
|
-
return yield* Layer.buildWithMemoMap(native, memoMap, scope).pipe(Effect.setContext(Context.make(HttpRouter.HttpRouter, router)));
|
|
126
|
-
}));
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
function layer(options, ...apps) {
|
|
130
|
-
return erasedLayer(apps, options);
|
|
20
|
+
}
|
|
21
|
+
function layerStdio(options, ...apps) {
|
|
22
|
+
return server(apps, McpServer.layerStdio(options));
|
|
131
23
|
}
|
|
132
24
|
//#endregion
|
|
133
|
-
export {
|
|
25
|
+
export { layerHttp, layerStdio };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Action_d_exports, Any } from "./Action.mjs";
|
|
2
|
+
import { AnyImplementation, BuildContext, BuildError, HandlerContext, Implementation } from "./internal/implementation.mjs";
|
|
3
|
+
import { Layer, Schema } from "effect";
|
|
4
|
+
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
5
|
+
//#region src/ActionToolkit.d.ts
|
|
6
|
+
/** A native tool corresponding to an MCP-enabled action. */
|
|
7
|
+
type NativeTool<A extends Any, R> = A extends {
|
|
8
|
+
readonly mcp: {
|
|
9
|
+
readonly name: infer Name extends string;
|
|
10
|
+
};
|
|
11
|
+
} ? Tool.Tool<Name, {
|
|
12
|
+
readonly parameters: A["input"];
|
|
13
|
+
readonly success: A["success"];
|
|
14
|
+
readonly failure: Schema.Union<A["errors"]>;
|
|
15
|
+
readonly failureMode: "return";
|
|
16
|
+
}, R> : never;
|
|
17
|
+
type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
|
|
18
|
+
type ToolForAction<App extends AnyImplementation, A extends Any> = A extends {
|
|
19
|
+
readonly mcp: object;
|
|
20
|
+
} ? NativeTool<A, HandlerContext<HandlersOf<App>, Extract<A["name"], keyof HandlersOf<App>>>> : never;
|
|
21
|
+
type ToolsFor<App extends AnyImplementation> = App extends AnyImplementation ? ToolForAction<App, App["group"]["actions"][number]> : never;
|
|
22
|
+
type ToolkitTools<Apps extends ReadonlyArray<AnyImplementation>> = { readonly [T in ToolsFor<Apps[number]> as T["name"]]: T; };
|
|
23
|
+
/** Native tools and the layer that binds their action implementations. */
|
|
24
|
+
export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R = never> {
|
|
25
|
+
readonly toolkit: Toolkit.Toolkit<Tools>;
|
|
26
|
+
/** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
|
|
27
|
+
readonly layer: Layer.Layer<Tool.HandlersFor<Tools>, E, R>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Project implementations into Effect's native AI toolkit.
|
|
31
|
+
*
|
|
32
|
+
* Unlike MCP, calls return the action's native success/failure values directly.
|
|
33
|
+
* Build services are needed to construct `layer`; request services are needed
|
|
34
|
+
* when the resulting toolkit handles a call.
|
|
35
|
+
*/
|
|
36
|
+
export declare function make<const Apps extends ReadonlyArray<AnyImplementation>>(...apps: Apps): Binding<ToolkitTools<Apps>, BuildError<Apps[number]>, BuildContext<Apps[number]>>;
|
|
37
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./internal/implementation.mjs";
|
|
2
|
+
import { bindTools } from "./internal/tools.mjs";
|
|
3
|
+
import "effect";
|
|
4
|
+
import "effect/unstable/ai";
|
|
5
|
+
//#region src/ActionToolkit.ts
|
|
6
|
+
function make(...apps) {
|
|
7
|
+
return bindTools(apps, "native");
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { make };
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import { i as Codec, n as Action_d_exports } from "./Action-CXsfTEhx.mjs";
|
|
2
1
|
import { Context, Effect } from "effect";
|
|
3
|
-
import {
|
|
2
|
+
import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
|
|
4
3
|
import { NonEmptyReadonlyArray } from "effect/Array";
|
|
5
4
|
//#region src/Authentication.d.ts
|
|
6
|
-
export interface Options<A, Errors extends ReadonlyArray<Codec>, R> {
|
|
7
|
-
readonly authenticate: Effect.Effect<A, Errors[number]["Type"], R>;
|
|
8
|
-
readonly errors: Errors;
|
|
9
|
-
readonly headers?: (error: Errors[number]["Type"]) => Headers.Input;
|
|
10
|
-
}
|
|
11
5
|
/**
|
|
12
6
|
* Authenticate each request and provide its identity to the downstream handler.
|
|
13
|
-
*
|
|
14
|
-
* Dependencies remain native router request
|
|
15
|
-
* until the request scope closes, including
|
|
7
|
+
* `authenticate` fails with the response to send instead, so the host owns its
|
|
8
|
+
* status, body and challenge headers. Dependencies remain native router request
|
|
9
|
+
* requirements. Acquired resources live until the request scope closes, including
|
|
10
|
+
* while the handler is running. Every response is marked `Cache-Control: no-store`,
|
|
11
|
+
* including private failures serialized by enclosing middleware.
|
|
16
12
|
*/
|
|
17
|
-
export declare const middleware: <I, A,
|
|
13
|
+
export declare const middleware: <I, A, R>(service: Context.Key<I, A>, authenticate: Effect.Effect<NoInfer<A>, HttpServerResponse.HttpServerResponse, R>) => HttpRouter.Middleware<{
|
|
18
14
|
provides: I;
|
|
19
15
|
handles: never;
|
|
20
16
|
error: never;
|
|
@@ -22,13 +18,15 @@ export declare const middleware: <I, A, const Errors extends ReadonlyArray<Codec
|
|
|
22
18
|
layerError: never;
|
|
23
19
|
layerRequires: never;
|
|
24
20
|
}>;
|
|
21
|
+
/** RFC 9728 metadata to publish; the host is responsible for these being valid OAuth URLs. */
|
|
25
22
|
export interface ProtectedResourceOptions {
|
|
26
|
-
/** Exact OAuth resource identifier
|
|
23
|
+
/** Exact OAuth resource identifier; its path and query select the discovery path. */
|
|
27
24
|
readonly resource: string;
|
|
28
25
|
readonly authorizationServers: NonEmptyReadonlyArray<string>;
|
|
29
26
|
readonly scopesSupported?: ReadonlyArray<string>;
|
|
30
27
|
readonly resourceName?: string;
|
|
31
28
|
}
|
|
29
|
+
/** Parameters of one `WWW-Authenticate: Bearer` challenge, RFC 6750 §3. */
|
|
32
30
|
export interface BearerChallengeOptions {
|
|
33
31
|
readonly error?: "invalid_token" | "insufficient_scope";
|
|
34
32
|
readonly errorDescription?: string;
|
|
@@ -42,6 +40,7 @@ export interface BearerChallengeOptions {
|
|
|
42
40
|
export declare const protectedResource: (options: ProtectedResourceOptions) => {
|
|
43
41
|
layer: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter>;
|
|
44
42
|
metadataUrl: string;
|
|
43
|
+
/** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
|
|
45
44
|
challenge: (challenge?: BearerChallengeOptions) => string;
|
|
46
45
|
};
|
|
47
46
|
//#endregion
|