@gjermundgaraba/effect-actions 0.14.0 → 0.16.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.
Files changed (55) hide show
  1. package/dist/Action.d.mts +1 -1
  2. package/dist/ActionCli.d.mts +4 -6
  3. package/dist/ActionCli.mjs +11 -9
  4. package/dist/ActionHttp.mjs +18 -30
  5. package/dist/ActionMcp.d.mts +23 -7
  6. package/dist/ActionMcp.mjs +78 -90
  7. package/dist/ActionRpc.mjs +17 -29
  8. package/dist/ActionToolkit.mjs +26 -11
  9. package/dist/Authentication.mjs +1 -2
  10. package/dist/Testing.d.mts +6 -73
  11. package/dist/Testing.mjs +3 -102
  12. package/dist/authentication/provider.mjs +1 -6
  13. package/dist/authentication/refusal.mjs +14 -4
  14. package/dist/cli/commands.mjs +8 -8
  15. package/dist/contract/call.d.mts +2 -1
  16. package/dist/contract/call.mjs +14 -1
  17. package/dist/contract/errors.d.mts +2 -3
  18. package/dist/contract/errors.mjs +6 -12
  19. package/dist/contract/implementation.mjs +33 -5
  20. package/dist/contract/rules.d.mts +7 -0
  21. package/dist/contract/rules.mjs +50 -3
  22. package/dist/http/client.mjs +4 -7
  23. package/dist/mcp/client.d.mts +87 -0
  24. package/dist/mcp/client.mjs +122 -0
  25. package/dist/mcp/protocol.mjs +25 -37
  26. package/docs/Action.md +6 -5
  27. package/docs/ActionCli.md +4 -3
  28. package/docs/ActionMcp-failures.md +7 -2
  29. package/docs/ActionMcp.md +43 -46
  30. package/docs/ActionToolkit-failures.md +2 -1
  31. package/docs/ActionToolkit.md +2 -2
  32. package/docs/Authentication-failures.md +2 -2
  33. package/docs/Authentication.md +10 -5
  34. package/docs/CONTEXT.md +1 -1
  35. package/docs/Testing-failures.md +3 -3
  36. package/docs/Testing.md +16 -24
  37. package/docs/guarantees.md +15 -14
  38. package/examples/kitchen-sink/README.md +26 -26
  39. package/examples/kitchen-sink/test/attachments/attachments.test.ts +3 -2
  40. package/examples/kitchen-sink/test/identity/identity.test.ts +13 -6
  41. package/examples/kitchen-sink/test/routes.test.ts +4 -3
  42. package/examples/live-rpc/README.md +7 -2
  43. package/examples/mcp-tools/test/routes.test.ts +7 -6
  44. package/examples/notes-service/README.md +13 -3
  45. package/examples/notes-service/src/notes/Notes.ts +4 -2
  46. package/examples/notes-service/src/routes.ts +24 -14
  47. package/examples/notes-service/src/worker.ts +20 -0
  48. package/examples/notes-service/test/commands.test.ts +2 -1
  49. package/examples/notes-service/test/routes.test.ts +6 -3
  50. package/examples/notes-service/test/worker.test.ts +34 -0
  51. package/examples/quickstart/test/greeting/greeting.test.ts +2 -1
  52. package/package.json +1 -1
  53. package/dist/http/declared.mjs +0 -20
  54. package/dist/mcp/protocol.d.mts +0 -18
  55. package/dist/toolkit/tools.mjs +0 -29
package/dist/Action.d.mts CHANGED
@@ -26,7 +26,7 @@ export interface Mcp {
26
26
  * `annotations.destructiveHint`, meaningful for a write only. Left out, a write is listed as
27
27
  * destructive, MCP's default: state `false` for one that only adds, MCP's "only additive
28
28
  * updates". One that replaces, removes or interrupts, such as an edit or a restart, stays
29
- * destructive.
29
+ * destructive, an update of a stored flag such as "read" included.
30
30
  */
31
31
  readonly destructiveHint?: boolean | undefined;
32
32
  /** `annotations.idempotentHint`, meaningful for a write only. */
@@ -1,19 +1,17 @@
1
+ import { Declaring } from "./contract/rules.mjs";
1
2
  import { BuiltIn } from "./contract/errors.mjs";
2
3
  import { ActionOf, BuildError, BuildServices, Holding, Known, Member, RequestOf, Selected, SelectedOf, Selection, Served } from "./contract/implementation.mjs";
3
- import { Action_d_exports, Any, Errors } from "./Action.mjs";
4
+ import { Action_d_exports, Any } from "./Action.mjs";
4
5
  import { VerifierError } from "./authentication/provider.mjs";
5
6
  import { Options, UserError } from "./cli/commands.mjs";
6
7
  import { AnyHttp, Options as Options$2 } from "./http/client.mjs";
8
+ import "./ActionHttp.mjs";
7
9
  import { Effect, Schema } from "effect";
8
10
  import { HttpClient, HttpClientError } from "effect/http";
9
11
  import { Command } from "effect/cli";
10
12
  //#region src/cli/ActionCli.d.ts
11
- interface AnyBinding {
12
- readonly authentication?: {
13
- readonly error: Errors;
14
- } | undefined;
13
+ interface AnyBinding extends Declaring {
15
14
  readonly actions: ReadonlyArray<Any>;
16
- readonly error: Errors;
17
15
  }
18
16
  /**
19
17
  * Make a command of your own fail as a derived command does. Give it what the command calls, the
@@ -1,7 +1,6 @@
1
- import { assertDistinct, assertKnown } from "./contract/rules.mjs";
1
+ import { assertDistinct, assertKnown, bindingErrors, projectedErrors } from "./contract/rules.mjs";
2
2
  import { Implementation, acquire, assertHeld, select, toList } from "./contract/implementation.mjs";
3
- import { withVerifierErrors } from "./authentication/provider.mjs";
4
- import { assertInBinding, methods } from "./http/client.mjs";
3
+ import { client } from "./ActionHttp.mjs";
5
4
  import { command as command$1, failingAsDerived } from "./cli/commands.mjs";
6
5
  import { Cause, Effect, Logger, Runtime } from "effect";
7
6
  import { Command } from "effect/cli";
@@ -17,8 +16,7 @@ import { Command } from "effect/cli";
17
16
  * `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result would
18
17
  * replace its JSON.
19
18
  */
20
- const toUserError = (calls) => "actions" in calls ? failingAsDerived(calls.actions, (action) => endpointErrorsOf(calls, action)) : failingAsDerived(calls, () => []);
21
- const endpointErrorsOf = (binding, action) => withVerifierErrors(action, binding.error, binding.authentication?.error);
19
+ const toUserError = (calls) => "actions" in calls ? failingAsDerived(calls.actions, (action) => bindingErrors(action, calls)) : failingAsDerived(calls, (action) => projectedErrors(action));
22
20
  const runWithBuildOfItsOwn = (app, action, input) => {
23
21
  const thisActionOnly = Implementation.share([action], app);
24
22
  return Effect.flatMap(acquire([thisActionOnly]), (bound) => {
@@ -34,11 +32,15 @@ const theImplementationOf = (apps, action) => {
34
32
  };
35
33
  const inProcess = (apps, action, options) => {
36
34
  const app = theImplementationOf(apps, action);
37
- return command$1(action, (input) => runWithBuildOfItsOwn(app, action, input), options);
35
+ return command$1(action, (input) => runWithBuildOfItsOwn(app, action, input), options, projectedErrors(action));
38
36
  };
39
- const overHttp = (binding, action, options, client) => {
40
- assertInBinding(binding.actions, action);
41
- return command$1(action, (input) => Effect.flatMap(methods(binding, client), (methodOf) => methodOf(action)(input)), options, endpointErrorsOf(binding, action));
37
+ const overHttp = (binding, action, options, client$1) => {
38
+ if (!binding.actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
39
+ return command$1(action, (input) => Effect.flatMap(client(binding, client$1), (methods) => {
40
+ const method = methods[action.name];
41
+ if (method === void 0) throw new Error(`No client method for ${action.name}`);
42
+ return method(input);
43
+ }), options, bindingErrors(action, binding));
42
44
  };
43
45
  const aggregateCommand = (name, actions, commands, own, project) => {
44
46
  const derived = actions.map((action) => ({
@@ -1,7 +1,7 @@
1
- import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./contract/rules.mjs";
2
- import { acquire, provideHandlers, servedBy, toList } from "./contract/implementation.mjs";
3
- import { assertAuthentication, providerOf, withVerifierErrors } from "./authentication/provider.mjs";
4
- import { declared } from "./http/declared.mjs";
1
+ import { Anyone, bindingErrors, bindingOf, declared, projectedErrors } from "./contract/rules.mjs";
2
+ import { acquire, provideHandlers, servingOf } from "./contract/implementation.mjs";
3
+ import { providerOf } from "./authentication/provider.mjs";
4
+ import { inputParseOptions } from "./contract/call.mjs";
5
5
  import { methods } from "./http/client.mjs";
6
6
  import { SchemaErrors, schemaErrors } from "./http/schema-errors.mjs";
7
7
  import { Context, Effect, Layer, Scope } from "effect";
@@ -19,28 +19,24 @@ const apiOf = (group, endpoints) => {
19
19
  const endpointOf = (action, path, errors) => HttpApiEndpoint.post(action.name, path, {
20
20
  payload: action.input.pipe(HttpApiSchema.asJson()),
21
21
  success: action.success,
22
- error: projectedErrors(action, errors).flatMap(declared)
22
+ error: errors.flatMap(declared)
23
23
  });
24
24
  const withinLayerMiddleware = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
25
25
  function make(actions, options = {}) {
26
- assertOnce("action", actions);
27
- const errors = errorList(options.error);
28
- assertOwnTags("ActionHttp binding", errors);
29
- assertAuthentication(actions, options.authentication);
26
+ const bound = bindingOf("ActionHttp binding", actions, options);
30
27
  const mount = mountSegments(options.prefix);
31
- const security = options.authentication?.["~middleware"];
28
+ const security = bound.authentication?.["~middleware"];
32
29
  const endpoints = actions.map((action) => {
33
30
  const isPublic = security === void 0 || action.caller === Anyone;
34
- const endpoint = endpointOf(action, absolutePath([...mount, action.name]), withVerifierErrors(action, errors, options.authentication?.error)).annotate(OpenApi.Description, action.description);
31
+ const endpoint = endpointOf(action, absolutePath([...mount, action.name]), bindingErrors(action, bound)).annotate(OpenApi.Description, action.description);
35
32
  return isPublic ? endpoint : endpoint.middleware(security);
36
33
  });
37
34
  const api = apiOf(groupName(mount), endpoints);
38
35
  return {
39
36
  actions,
40
- error: errors,
37
+ ...bound,
41
38
  prefix: absolutePath(mount),
42
- api,
43
- authentication: options.authentication
39
+ api
44
40
  };
45
41
  }
46
42
  const buildInEmptyContext = (group) => Layer.fromBuild((memoMap, scope) => Effect.setContext(Layer.buildWithMemoMap(group, memoMap, scope), Context.empty()));
@@ -52,40 +48,32 @@ var StepUp = class extends HttpApiMiddleware.Service()("effect-actions/ActionHtt
52
48
  var JsonContentType = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/JsonContentType") {};
53
49
  const jsonContentType = () => Layer.succeed(JsonContentType, (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.headers["content-type"] === void 0 ? Effect.succeed(HttpServerResponse.text("Unsupported content-type: none", { status: 415 })) : route));
54
50
  function layer(binding, served, options = {}) {
55
- const apps = servedBy("HTTP binding", binding.actions, toList(served), options.actions);
56
- const actions = apps.flatMap((app) => app.actions);
57
- assertAuthentication(actions, binding.authentication);
58
- assertDistinct("middleware", options.middleware ?? [], (key) => key.key);
51
+ const { apps, actions, authentication } = servingOf("HTTP binding", binding, served, options);
59
52
  const mount = mountSegments(binding.prefix);
60
53
  const name = groupName(mount);
61
54
  const api = apiOf(name, actions.map((action) => {
62
- const endpoint = withinLayerMiddleware(endpointOf(action, absolutePath([...mount, action.name]), binding.error).middleware(JsonContentType).middleware(SchemaErrors), options.middleware);
63
- return action.caller === Anyone || binding.authentication === void 0 ? endpoint : endpoint.middleware(StepUp).middleware(binding.authentication["~middleware"]);
64
- })).annotate(HttpApi.PayloadParseOptions, {
65
- errors: "all",
66
- onExcessProperty: "error"
67
- });
55
+ const endpoint = withinLayerMiddleware(endpointOf(action, absolutePath([...mount, action.name]), projectedErrors(action, binding.error)).middleware(JsonContentType).middleware(SchemaErrors), options.middleware);
56
+ return action.caller === Anyone || authentication === void 0 ? endpoint : endpoint.middleware(StepUp).middleware(authentication["~middleware"]);
57
+ })).annotate(HttpApi.PayloadParseOptions, inputParseOptions);
68
58
  const handlers = Layer.unwrap(Effect.gen(function* () {
69
59
  const bound = yield* acquire(apps);
70
- const auth = actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0;
71
- const authentication = auth === void 0 ? Layer.empty : yield* Effect.map(providerOf(auth), (provider) => Layer.mergeAll(Layer.succeed(auth["~middleware"], provider.middleware), Layer.succeed(StepUp, provider.stepUp)));
60
+ const security = authentication === void 0 ? Layer.empty : yield* Effect.map(providerOf(authentication), (provider) => Layer.mergeAll(Layer.succeed(authentication["~middleware"], provider.middleware), Layer.succeed(StepUp, provider.stepUp)));
72
61
  const hostProvidedMiddleware = yield* Effect.forEach(options.middleware ?? [], (key) => Effect.map(Effect.service(key), (service) => Layer.succeed(key, service)));
73
62
  const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, (request) => run(request.payload)]));
74
63
  return HttpApiBuilder.group(api, name, (builder) => builder.handleAll(byName)).pipe(Layer.provide([
75
64
  schemaErrors(),
76
65
  jsonContentType(),
77
- authentication,
66
+ security,
78
67
  ...hostProvidedMiddleware
79
68
  ]), buildInEmptyContext);
80
69
  })).pipe(provideHandlers(apps));
81
70
  return HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(startupBeneathRequest()));
82
71
  }
83
- const erasedClient = (binding, options) => Effect.map(methods(binding, options), (methodOf) => Object.fromEntries(binding.actions.map((action) => [action.name, methodOf(action)])));
84
72
  function client(binding, options) {
85
- return erasedClient(binding, options);
73
+ return methods(binding, options);
86
74
  }
87
75
  function fetchClient(binding, { fetch, ...options } = {}) {
88
- return erasedClient(binding, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch ?? ((input, init) => globalThis.fetch(input, init))), Effect.runSync);
76
+ return methods(binding, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch ?? ((input, init) => globalThis.fetch(input, init))), Effect.runSync);
89
77
  }
90
78
  //#endregion
91
79
  export { client, fetchClient, layer, make };
@@ -1,6 +1,7 @@
1
1
  import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Protected, SelectedOf, Selection, Served, ServedRequest } from "./contract/implementation.mjs";
2
2
  import { Action_d_exports, Any } from "./Action.mjs";
3
3
  import { Any as Any$1, Matching, ProviderOf, RemoteRequest, Required, ServerBuildServices } from "./authentication/provider.mjs";
4
+ import { Client, ClientOptions, McpCallError, Params, RequestOptions, client, request } from "./mcp/client.mjs";
4
5
  import { Cause, Effect, Layer, Stdio } from "effect";
5
6
  import { McpSchema, McpServer } from "effect/ai";
6
7
  import { HttpRouter } from "effect/http";
@@ -24,16 +25,29 @@ export interface Options<E = never, R = never, A extends Any = Any> extends Omit
24
25
  readonly features?: Layer.Layer<never, E, R> | undefined;
25
26
  }
26
27
  /**
27
- * One Streamable HTTP MCP endpoint, speaking MCP 2026-07-28, 2025-11-25 or 2025-06-18, as the
28
- * client negotiates: `Options`, and every native `McpServer.layerHttp` option, `allowedOrigins`
29
- * among them, except `protocols` and `allowSessionTermination`, which are fixed, with `path`
30
- * defaulting to `/mcp`. A client of a 2025 revision opens a session with `initialize`, which
31
- * lasts until that client ends it with a DELETE, answered 204, or the process stops.
28
+ * One Streamable HTTP MCP endpoint: `Options`, `authentication`, `requestScoped`, and every native
29
+ * `McpServer.layerHttp` option, `allowedOrigins` among them, except `protocols` and
30
+ * `allowSessionTermination`, which are fixed.
32
31
  */
33
32
  export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> extends Options<E, R, A>, Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols" | "path" | "allowSessionTermination"> {
34
33
  /** The endpoint's route; defaults to `/mcp`. */
35
34
  readonly path?: HttpRouter.PathInput;
36
35
  readonly authentication?: Any$1;
36
+ /**
37
+ * Whether MCP 2026-07-28 clients may subscribe, holding a stream open for list changes and
38
+ * resource updates. Defaults to whether `features` are given: an endpoint of actions alone
39
+ * registers its tools once, before any client connects, and serves no resource. Pass `true`
40
+ * where a handler changes the server through `McpServer.McpServer`.
41
+ */
42
+ readonly allowSubscriptions?: boolean | undefined;
43
+ /**
44
+ * `true` runs a server per request, as Effect's `McpServer.layerHttpRequestScoped` does, for a
45
+ * host such as Cloudflare Workers where nothing may outlive a request. It speaks MCP 2026-07-28
46
+ * alone, so a client that opens with `initialize` is refused, and it keeps no session. It has
47
+ * no subscriptions, so `allowSubscriptions` is ignored, and native `features` send no
48
+ * list-change notification and cannot use `McpServer.elicit`.
49
+ */
50
+ readonly requestScoped?: boolean | undefined;
37
51
  }
38
52
  type ToolRequestContext<R> = Exclude<R, McpSchema.McpRequestContext | McpServer.McpServer>;
39
53
  type HttpToolRequestContext<R> = Exclude<ToolRequestContext<R>, HttpRouter.Provided>;
@@ -47,7 +61,8 @@ type Inferring<D, A extends Any> = [D] extends [undefined] ? [Protected<A>] exte
47
61
  * Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28, 2025-11-25 or
48
62
  * 2025-06-18, as the client negotiates. A 2026-07-28 request stands alone; a client of a 2025
49
63
  * revision opens a session with `initialize`, which holds what it sent there and no identity,
50
- * and lasts until that client ends it with a DELETE, answered 204, or the process stops.
64
+ * and lasts until that client ends it with a DELETE, answered 204, or the process stops. For a
65
+ * serverless host, see `requestScoped`.
51
66
  *
52
67
  * Protected tools need the `authentication` descriptor their contracts name, and the
53
68
  * provider `Authentication.layer` builds from it. On an endpoint of protected tools only,
@@ -76,4 +91,5 @@ export declare function layerHttp<const Apps extends Served, const O extends Sel
76
91
  * request identity or authority; each implementation's `authorize` runs for its protected actions.
77
92
  */
78
93
  export declare function runStdio<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never>(implementations: Apps, options: Options<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<E, R, Any>>>): Effect.Effect<void, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Stdio.Stdio | ToolRequestContext<ServedRequest<Member<Apps>, SelectedOf<O, Apps>>> | Features<R>>;
79
- //#endregion
94
+ //#endregion
95
+ export { type Client, type ClientOptions, McpCallError, type Params, type RequestOptions, client, request };
@@ -1,20 +1,18 @@
1
- import { Anyone } from "./contract/rules.mjs";
2
- import { provideHandlers, select, toList } from "./contract/implementation.mjs";
3
- import { bindTools } from "./toolkit/tools.mjs";
4
- import { fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, protocols } from "./mcp/protocol.mjs";
5
- import { assertAuthentication, promote, providerOf } from "./authentication/provider.mjs";
1
+ import { Anyone, assertAuthentication, assertOnce, hintsOf, projectedErrors } from "./contract/rules.mjs";
2
+ import { acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
3
+ import { promote, providerOf } from "./authentication/provider.mjs";
4
+ import { inputParseOptions } from "./contract/call.mjs";
5
+ import { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, protocols } from "./mcp/protocol.mjs";
6
6
  import { recordStepUp } from "./authentication/refusal.mjs";
7
7
  import { withStderrConsole } from "./stdio/console.mjs";
8
- import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, References, Result, Schema, Stream } from "effect";
9
- import { AiError, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
8
+ import { McpCallError, client, request } from "./mcp/client.mjs";
9
+ import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Predicate, Schema } from "effect";
10
+ import { AiError, McpSchema, McpServer } from "effect/ai";
10
11
  import { HttpRouter, HttpServerRequest } from "effect/http";
12
+ import { Base64 } from "effect/encoding";
11
13
  //#region src/mcp/ActionMcp.ts
12
- const strictToolProjection = {
13
- label: "MCP tool",
14
- tool: (tool) => tool.annotate(Tool.Strict, true)
15
- };
16
14
  const isToolArgumentsSchema = Schema.is(McpSchema.ToolJson);
17
- const nativeToolJsonSchema = (schema, strict) => {
15
+ const toolJsonSchema = (schema, strict) => {
18
16
  const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
19
17
  const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
20
18
  const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
@@ -24,7 +22,7 @@ const nativeToolJsonSchema = (schema, strict) => {
24
22
  };
25
23
  };
26
24
  const assertToolShapes = (lifted) => {
27
- const inputs = lifted.map(([action]) => action).filter((action) => !isToolArgumentsSchema(nativeToolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
25
+ const inputs = lifted.map(([action]) => action).filter((action) => !isToolArgumentsSchema(toolJsonSchema(Schema.toCodecJson(action.input), true))).map(({ name }) => name);
28
26
  if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
29
27
  const misplaced = lifted.filter(([action, lift]) => isMisplaced(action, lift)).map(([{ name }]) => name);
30
28
  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(", ")}`);
@@ -47,99 +45,84 @@ const contentBlockOf = ({ kind, field, value }) => ({
47
45
  mimeType: value.mimeType,
48
46
  ...field === void 0 ? {} : { _meta: { [fieldKey]: field } }
49
47
  });
50
- const parameterValidationErrorOf = (error) => AiError.isAiError(error) && Predicate.isTagged(error.reason, "ToolParameterValidationError") ? error.reason : void 0;
51
- const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
52
- const registerToolsLiftingMedia = Effect.fnUntraced(function* (toolkit, lifts) {
48
+ const toolOf = (action, outputSchema) => Effect.map(Schema.decodeUnknownEffect(McpSchema.ToolJson)(toolJsonSchema(Schema.toCodecJson(action.input), true)), (inputSchema) => new McpSchema.Tool({
49
+ name: action.name,
50
+ description: action.description,
51
+ inputSchema,
52
+ outputSchema,
53
+ annotations: {
54
+ ...action.mcp.title === void 0 ? {} : { title: action.mcp.title },
55
+ ...hintsOf(action)
56
+ },
57
+ _meta: action.mcp._meta === void 0 ? void 0 : { ...action.mcp._meta }
58
+ }));
59
+ const registerTools = Effect.fnUntraced(function* (apps, authentication) {
53
60
  const registry = yield* McpServer.McpServer;
54
- const built = yield* toolkit.pipe(Effect.updateContext((context) => Context.merge(context, Context.mergeAll(...Object.values(toolkit.tools).flatMap((tool) => {
55
- const key = Context.Service(tool.id);
56
- return Option.toArray(Context.getOption(context, key)).map((handler) => Context.make(key, {
57
- ...handler,
58
- context: omitRequestServices(handler.context)
59
- }));
60
- })))));
61
- const retainedServices = omitRequestServices(yield* Effect.context());
62
- const propagateInterruptOrReportInternal = (cause) => {
63
- const failure = Cause.findFail(cause);
64
- return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(Effect.provideContext(ErrorReporter.report(cause), retainedServices)), Effect.as(textErrorResult(nativeInternalErrorText)));
65
- };
66
- const invalidParamsOrInternal = (cause) => {
67
- const failure = Cause.findFail(cause);
68
- if (Result.isSuccess(failure)) {
69
- const { error } = failure.success;
70
- const invalid = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin) === "parameters" ? parameterValidationErrorOf(error) : void 0;
71
- if (invalid !== void 0) return Effect.fail(new McpSchema.InvalidParams({ message: invalid.message }));
72
- }
73
- return propagateInterruptOrReportInternal(cause);
74
- };
75
- const registrations = yield* Effect.forEach(Object.entries(built.tools), ([name, tool]) => Effect.gen(function* () {
76
- const strict = Tool.getStrictMode(tool) === true;
77
- const decodeOptions = {
78
- onExcessProperty: strict ? "error" : "ignore",
79
- errors: "all"
61
+ const bound = yield* acquire(apps);
62
+ const built = yield* Effect.context();
63
+ const internalError = (cause) => Effect.logError(cause).pipe(Effect.andThen(ErrorReporter.report(cause)), Effect.as(textErrorResult(nativeInternalErrorText)));
64
+ const internalErrorOnDefect = (cause) => Cause.hasDies(cause) ? internalError(cause) : Effect.failCause(cause);
65
+ for (const [action, run] of bound) {
66
+ const lift = liftOf(action.success);
67
+ const media = lift.fields.length === 0 ? void 0 : lift;
68
+ const structured = media === void 0 ? Schema.toCodecJson(action.success) : media.rest === void 0 ? void 0 : Schema.toCodecJson(media.rest);
69
+ const outputSchema = structured === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(toolJsonSchema(structured, false)).pipe(Effect.orDie);
70
+ const tool = yield* toolOf(action, outputSchema).pipe(Effect.orDie);
71
+ const decode = Schema.decodeUnknownEffect(Schema.toCodecJson(action.input), inputParseOptions);
72
+ const encoder = (schema) => {
73
+ const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(schema));
74
+ return (value) => Effect.orDie(encode(value));
80
75
  };
81
- const lift = lifts.get(name);
82
- const media = lift === void 0 || lift.fields.length === 0 ? void 0 : lift;
83
- const structured = media === void 0 ? tool.successSchema : media.rest === void 0 ? void 0 : Schema.toCodecJson(media.rest);
84
- const outputSchema = structured === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(nativeToolJsonSchema(structured, false)).pipe(Effect.orDie);
85
- const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(nativeToolJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
76
+ const encodeSuccess = encoder(action.success);
77
+ const encodeFailure = encoder(Schema.Union(projectedErrors(action)));
78
+ const call = authentication === void 0 || action.caller === Anyone ? run : (input) => recordStepUp(promote(authentication, run(input)));
86
79
  const liftedFieldNames = new Set(media?.fields.map(({ name }) => name));
87
- const resultWithLiftedMedia = ({ encodedResult, result }) => {
80
+ const resultOf = (encoded, success) => {
88
81
  if (media === void 0) return new McpSchema.CallToolResult({
89
82
  isError: false,
90
- structuredContent: encodedResult,
91
- content: nativeJsonTextOf(encodedResult)
83
+ structuredContent: encoded,
84
+ content: nativeJsonTextOf(encoded)
92
85
  });
93
- const blocks = liftedFrom(media, result).map(contentBlockOf);
86
+ const blocks = liftedFrom(media, success).map(contentBlockOf);
94
87
  if (structured === void 0) return new McpSchema.CallToolResult({
95
88
  isError: false,
96
89
  content: blocks
97
90
  });
98
- const encodedRestOfStruct = Predicate.isObject(encodedResult) ? Object.fromEntries(Object.entries(encodedResult).filter(([key]) => !liftedFieldNames.has(key))) : encodedResult;
91
+ const encodedRestOfStruct = Predicate.isObject(encoded) ? Object.fromEntries(Object.entries(encoded).filter(([key]) => !liftedFieldNames.has(key))) : encoded;
99
92
  return new McpSchema.CallToolResult({
100
93
  isError: false,
101
94
  structuredContent: encodedRestOfStruct,
102
95
  content: [...nativeJsonTextOf(encodedRestOfStruct), ...blocks]
103
96
  });
104
97
  };
105
- return {
106
- tool: new McpSchema.Tool({
107
- name,
108
- description: Tool.getDescription(tool),
109
- inputSchema,
110
- outputSchema,
111
- annotations: {
112
- ...Context.getOption(tool.annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
113
- readOnlyHint: Context.get(tool.annotations, Tool.Readonly),
114
- destructiveHint: Context.get(tool.annotations, Tool.Destructive),
115
- idempotentHint: Context.get(tool.annotations, Tool.Idempotent),
116
- openWorldHint: Context.get(tool.annotations, Tool.OpenWorld)
117
- },
118
- _meta: Context.getOrUndefined(tool.annotations, Tool.Meta)
119
- }),
120
- annotations: tool.annotations,
121
- 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(resultWithLiftedMedia(result)) : result.failureOrigin === "handler" ? Effect.succeed(new McpSchema.CallToolResult({
122
- isError: true,
123
- content: nativeJsonTextOf(result.encodedResult)
124
- })) : Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")))), Effect.catchCause(invalidParamsOrInternal), Effect.provideContext(retainedServices))
125
- };
126
- }));
127
- for (const registration of registrations) yield* registry.addTool(registration);
98
+ yield* registry.addTool({
99
+ tool,
100
+ annotations: Context.empty(),
101
+ handle: (payload) => Effect.annotateCurrentSpan({
102
+ tool: action.name,
103
+ parameters: payload ?? {}
104
+ }).pipe(Effect.andThen(() => decode(payload ?? {})), Effect.mapError(({ message }) => new McpSchema.InvalidParams({ message: new AiError.ToolParameterValidationError({
105
+ toolName: action.name,
106
+ description: message
107
+ }).message })), Effect.flatMap((input) => call(input).pipe(Effect.matchEffect({
108
+ onFailure: (error) => Effect.map(encodeFailure(error), (encoded) => new McpSchema.CallToolResult({
109
+ isError: true,
110
+ content: nativeJsonTextOf(encoded)
111
+ })),
112
+ onSuccess: (success) => Effect.map(encodeSuccess(success), (encoded) => resultOf(encoded, success))
113
+ }))), Effect.catchCause(internalErrorOnDefect), Effect.updateContext((request) => Context.merge(built, request)))
114
+ });
115
+ }
128
116
  });
129
117
  const server = (apps, transport, features = Layer.empty, authentication) => {
130
- const binding = bindTools(apps, authentication === void 0 ? strictToolProjection : {
131
- ...strictToolProjection,
132
- handler: (run, action) => (input) => action.caller === Anyone ? run(input) : recordStepUp(promote(authentication, run(input)))
133
- });
118
+ assertOnce("MCP tool", apps.flatMap((app) => app.actions));
134
119
  const lifted = apps.flatMap((app) => app.actions.map((action) => [action, liftOf(action.success)]));
135
120
  assertToolShapes(lifted);
136
- const lifts = new Map(lifted.map(([{ name }, lift]) => [name, lift]));
121
+ const names = new Set(lifted.map(([{ name }]) => name));
137
122
  const register = Effect.gen(function* () {
138
- const registry = yield* McpServer.McpServer;
139
- const namesClaimedByFeatures = registry.tools.flatMap(({ tool }) => lifts.has(tool.name) ? [tool.name] : []);
123
+ const namesClaimedByFeatures = (yield* McpServer.McpServer).tools.flatMap(({ tool }) => names.has(tool.name) ? [tool.name] : []);
140
124
  if (namesClaimedByFeatures.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${namesClaimedByFeatures.join(", ")}, claimed by an action and a native feature`));
141
- const handlers = yield* Layer.build(binding.layer);
142
- yield* registerToolsLiftingMedia(binding.toolkit, lifts).pipe(Effect.setContext(Context.add(handlers, McpServer.McpServer, registry)));
125
+ yield* registerTools(apps, authentication);
143
126
  });
144
127
  return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
145
128
  };
@@ -149,7 +132,7 @@ const anonymousMethods = /* @__PURE__ */ new Set([
149
132
  "notifications/cancelled"
150
133
  ]);
151
134
  const requiresAuthentication = (actions) => {
152
- const open = new Set(actions.filter(({ caller }) => caller === Anyone).map(({ name }) => name));
135
+ const open = new Set(actions.filter(({ caller }) => caller === Anyone).flatMap(({ name }) => [name, `=?base64?${Base64.encode(name)}?=`]));
153
136
  return (headers) => {
154
137
  if (open.size === 0) return true;
155
138
  if (headers["mcp-protocol-version"] !== httpProtocol.protocolVersion) return true;
@@ -165,15 +148,20 @@ const namesItsMethod = (request) => Effect.match(request.json, {
165
148
  onFailure: () => true,
166
149
  onSuccess: (body) => !Predicate.hasProperty(body, "method") || body.method === request.headers["mcp-method"]
167
150
  });
168
- function layerHttp(apps, { actions, features, path, authentication, ...options }) {
151
+ function layerHttp(apps, { actions, features, path = defaultPath, authentication, requestScoped, allowSubscriptions = features !== void 0, ...options }) {
169
152
  const selected = select(toList(apps), actions);
170
153
  const served = selected.flatMap((app) => app.actions);
171
154
  assertAuthentication(served, authentication);
172
- const transport = server(selected, McpServer.layerHttp({
155
+ const transport = server(selected, requestScoped === true ? McpServer.layerHttpRequestScoped({
156
+ ...options,
157
+ path,
158
+ protocols: [httpProtocol]
159
+ }) : McpServer.layerHttp({
173
160
  ...options,
174
- path: path ?? "/mcp",
161
+ path,
175
162
  protocols,
176
- allowSessionTermination: true
163
+ allowSessionTermination: true,
164
+ allowSubscriptions
177
165
  }), features, authentication);
178
166
  if (authentication === void 0) return transport;
179
167
  const required = requiresAuthentication(served);
@@ -188,4 +176,4 @@ function runStdio(apps, { actions, features, ...options }) {
188
176
  return Layer.launch(transport).pipe(withStderrConsole, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
189
177
  }
190
178
  //#endregion
191
- export { layerHttp, runStdio };
179
+ export { McpCallError, client, layerHttp, request, runStdio };
@@ -1,28 +1,23 @@
1
- import { InvalidInput } from "./contract/errors.mjs";
2
- import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./contract/rules.mjs";
3
- import { acquire, provideHandlers, servedBy, toList } from "./contract/implementation.mjs";
4
- import { checked } from "./contract/call.mjs";
5
- import { assertAuthentication, providerOf, withVerifierErrors } from "./authentication/provider.mjs";
1
+ import { Anyone, bindingErrors, bindingOf } from "./contract/rules.mjs";
2
+ import { acquire, provideHandlers, servingOf } from "./contract/implementation.mjs";
3
+ import { providerOf } from "./authentication/provider.mjs";
4
+ import { checked, decodeInput } from "./contract/call.mjs";
6
5
  import { Cause, Effect, ErrorReporter, Layer, Predicate, Result, Schema, SchemaTransformation } from "effect";
7
6
  import { HttpClientRequest, HttpServerRequest } from "effect/http";
8
7
  import { HttpApiBuilder } from "effect/http-api";
9
8
  import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer } from "effect/rpc";
10
9
  //#region src/rpc/ActionRpc.ts
11
- const declaredErrorsOf = (action, errors, authentication) => projectedErrors(action, withVerifierErrors(action, errors, authentication?.error));
12
10
  function make(actions, options = {}) {
13
- assertOnce("action", actions);
14
- const errors = errorList(options.error);
15
- assertOwnTags("ActionRpc binding", errors);
16
- assertAuthentication(actions, options.authentication);
11
+ const bound = bindingOf("ActionRpc binding", actions, options);
12
+ const group = RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
13
+ payload: action.input,
14
+ success: action.success,
15
+ error: Schema.Union(bindingErrors(action, bound))
16
+ })));
17
17
  return {
18
18
  actions,
19
- error: errors,
20
- group: RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
21
- payload: action.input,
22
- success: action.success,
23
- error: Schema.Union(declaredErrorsOf(action, errors, options.authentication))
24
- }))),
25
- authentication: options.authentication
19
+ ...bound,
20
+ group
26
21
  };
27
22
  }
28
23
  const credentialFromRpcHeaders = (security, headers) => HttpApiBuilder.securityDecode(security).pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, HttpServerRequest.fromClientRequest(HttpClientRequest.get("/", { headers }))), Effect.provideService(HttpServerRequest.ParsedSearchParams, {}));
@@ -60,14 +55,11 @@ const callBoundaryOf = (error) => {
60
55
  });
61
56
  };
62
57
  const handlerOf = (action, run, steps, error) => {
63
- const decodeStrictly = Schema.decodeUnknownEffect(Schema.toCodecJson(action.input), {
64
- errors: "all",
65
- onExcessProperty: "error"
66
- });
58
+ const decode = decodeInput(action);
67
59
  const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.success));
68
60
  const boundary = callBoundaryOf(error);
69
61
  return (payload, options) => {
70
- const call = Effect.suspend(() => decodeStrictly(payload)).pipe(Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(run), Effect.flatMap(encode));
62
+ const call = Effect.suspend(() => decode(payload)).pipe(Effect.flatMap(run), Effect.flatMap(encode));
71
63
  const wrapped = steps.reduce((inner, step) => Effect.suspend(() => step(inner, {
72
64
  ...options,
73
65
  payload
@@ -76,12 +68,8 @@ const handlerOf = (action, run, steps, error) => {
76
68
  };
77
69
  };
78
70
  function layer(binding, served, options = {}) {
79
- const apps = servedBy("RPC binding", binding.actions, toList(served), options.actions);
80
- const actions = apps.flatMap((app) => app.actions);
81
- assertAuthentication(actions, binding.authentication);
71
+ const { apps, actions, authentication } = servingOf("RPC binding", binding, served, options);
82
72
  const middleware = options.middleware ?? [];
83
- assertDistinct("middleware", middleware, (key) => key.key);
84
- const auth = actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0;
85
73
  const jsonGroup = RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
86
74
  payload: Schema.Json,
87
75
  success: Schema.Json,
@@ -91,9 +79,9 @@ function layer(binding, served, options = {}) {
91
79
  const { codecFor } = yield* RpcServer.Protocol;
92
80
  if (codecFor !== RpcSerialization.json.codecFor) return yield* Effect.die(/* @__PURE__ */ new Error("ActionRpc serves a protocol speaking JSON: RpcSerialization.layerJson, layerNdjson, layerJsonRpc or layerNdJsonRpc, not another codec, such as schema-binary's"));
93
81
  const listedMiddleware = yield* Effect.forEach(middleware, (key) => Effect.service(key));
94
- const authenticate = auth === void 0 ? void 0 : yield* authenticationStepOf(auth);
82
+ const authenticate = authentication === void 0 ? void 0 : yield* authenticationStepOf(authentication);
95
83
  const bound = yield* acquire(apps);
96
- const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, handlerOf(action, run, action.caller === Anyone || authenticate === void 0 ? listedMiddleware : [...listedMiddleware, authenticate], Schema.Union(declaredErrorsOf(action, binding.error, binding.authentication)))]));
84
+ const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, handlerOf(action, run, action.caller === Anyone || authenticate === void 0 ? listedMiddleware : [...listedMiddleware, authenticate], Schema.Union(bindingErrors(action, binding)))]));
97
85
  return jsonGroup.toLayer(byName);
98
86
  })).pipe(provideHandlers(apps));
99
87
  return RpcServer.layer(jsonGroup, { disableFatalDefects: true }).pipe(Layer.provide(handlers));
@@ -1,20 +1,35 @@
1
- import { Implementation, provideHandlers, select, toList } from "./contract/implementation.mjs";
2
- import { bindTools } from "./toolkit/tools.mjs";
1
+ import { assertOnce, hintsOf, projectedErrors } from "./contract/rules.mjs";
2
+ import { Implementation, acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
3
+ import { Effect, Schema } from "effect";
4
+ import { Tool, Toolkit } from "effect/ai";
3
5
  //#region src/toolkit/ActionToolkit.ts
6
+ const annotateIfGiven = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
7
+ const annotateFromContract = (tool, action) => {
8
+ const { readOnlyHint, destructiveHint, idempotentHint, openWorldHint } = hintsOf(action);
9
+ const { title, _meta } = action.mcp;
10
+ const hinted = tool.annotate(Tool.Readonly, readOnlyHint).annotate(Tool.Destructive, destructiveHint).annotate(Tool.Idempotent, idempotentHint).annotate(Tool.OpenWorld, openWorldHint);
11
+ return annotateIfGiven(annotateIfGiven(hinted, Tool.Title, title), Tool.Meta, _meta === void 0 ? void 0 : { ..._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
+ });
4
20
  function make(apps, options) {
5
21
  const served = select(toList(apps), options?.actions);
6
22
  const needsApproval = options?.needsApproval;
7
- const { toolkit, layer } = bindTools(served, {
8
- label: "tool",
9
- tool: (tool, action, app) => Object.assign(needsApproval === void 0 ? tool : tool.setNeedsApproval((input, context) => needsApproval({
10
- name: action.name,
11
- action,
12
- input
13
- }, context)), { id: `${Implementation.runKey(app)}/${action.name}` })
14
- });
23
+ assertOnce("tool", served.flatMap((app) => app.actions));
24
+ const toolOf = (action, app) => Object.assign(needsApproval === void 0 ? jsonToolOf(action) : jsonToolOf(action).setNeedsApproval((input, context) => needsApproval({
25
+ name: action.name,
26
+ action,
27
+ input
28
+ }, context)), { id: `${Implementation.runKey(app)}/${action.name}` });
29
+ const toolkit = Toolkit.make(...served.flatMap((app) => app.actions.map((action) => annotateFromContract(toolOf(action, app), action))));
15
30
  return {
16
31
  toolkit,
17
- layer: layer.pipe(provideHandlers(served))
32
+ layer: toolkit.toLayer(Effect.map(acquire(served), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, run])))).pipe(provideHandlers(served))
18
33
  };
19
34
  }
20
35
  /**
@@ -1,8 +1,7 @@
1
1
  import { Unauthenticated, scopeToken } from "./contract/errors.mjs";
2
2
  import { assertOwnTags, errorList } from "./contract/rules.mjs";
3
3
  import { providerOf } from "./authentication/provider.mjs";
4
- import { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain } from "./authentication/refusal.mjs";
5
- import { declaredResponse } from "./http/declared.mjs";
4
+ import { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, isStepUp, plain } from "./authentication/refusal.mjs";
6
5
  import { Context, Effect, Layer, Match, Option, Predicate, Redacted } from "effect";
7
6
  import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
8
7
  import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";