@gjermundgaraba/effect-actions 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Action.d.mts +3 -5
- package/dist/Action.mjs +3 -1
- package/dist/ActionCli.d.mts +2 -2
- package/dist/ActionCli.mjs +2 -1
- package/dist/ActionHttp.d.mts +3 -2
- 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 +3 -2
- package/dist/cli/commands.mjs +2 -1
- 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 +3 -2
- package/dist/mcp/client.d.mts +40 -7
- package/dist/mcp/client.mjs +60 -17
- package/dist/mcp/protocol.mjs +1 -1
- package/docs/Action.md +17 -3
- package/docs/ActionCli.md +1 -1
- package/docs/ActionMcp.md +2 -2
- 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/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 +32 -0
- 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/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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { expect, it } from "@effect/vitest";
|
|
2
|
+
import { Arbitrary, Array, Effect, References, Schema } from "effect";
|
|
3
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
4
|
+
import { CurrentUser } from "../../src/identity/Identity.js";
|
|
5
|
+
import { users } from "../../src/identity/users.js";
|
|
6
|
+
import { notes } from "../../src/notes/handlers.js";
|
|
7
|
+
import { CreateNote, RequestConflict } from "../../src/notes/Notes.js";
|
|
8
|
+
import { NoteStore } from "../../src/notes/store.js";
|
|
9
|
+
|
|
10
|
+
const as = (user: keyof typeof users) => Effect.provideService(CurrentUser, users[user]);
|
|
11
|
+
|
|
12
|
+
it.effect("creates a new note on every call without a request key", () =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const client = yield* Action.client(notes);
|
|
15
|
+
const input = { title: "Plan", body: "Draft" };
|
|
16
|
+
const first = yield* client.createNote(input);
|
|
17
|
+
const second = yield* client.createNote(input);
|
|
18
|
+
|
|
19
|
+
expect(second.id).not.toBe(first.id);
|
|
20
|
+
expect(yield* client.listNotes()).toEqual({
|
|
21
|
+
notes: [
|
|
22
|
+
{ id: first.id, title: input.title },
|
|
23
|
+
{ id: second.id, title: input.title },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
}).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
it.effect.prop(
|
|
30
|
+
"replays a creation with the same key and content",
|
|
31
|
+
{
|
|
32
|
+
input: Arbitrary.schema(
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
...CreateNote.input.fields,
|
|
35
|
+
requestId: Schema.requiredKey(CreateNote.input.fields.requestId),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
({ input }) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const client = yield* Action.client(notes);
|
|
42
|
+
const first = yield* client.createNote(input);
|
|
43
|
+
const replay = yield* client.createNote(input);
|
|
44
|
+
|
|
45
|
+
expect(first).toMatchObject({ title: input.title, body: input.body, revision: 1 });
|
|
46
|
+
expect(replay).toEqual(first);
|
|
47
|
+
expect(yield* client.listNotes()).toEqual({
|
|
48
|
+
notes: [{ id: first.id, title: input.title }],
|
|
49
|
+
});
|
|
50
|
+
}).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
it.effect.each([
|
|
54
|
+
{ title: "Changed", body: "Draft" },
|
|
55
|
+
{ title: "Plan", body: "Changed" },
|
|
56
|
+
])("refuses reuse of a key with changed content: %j", (changed) =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const client = yield* Action.client(notes);
|
|
59
|
+
const input = { requestId: "plan", title: "Plan", body: "Draft" };
|
|
60
|
+
const first = yield* client.createNote(input);
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
yield* Effect.flip(client.createNote({ requestId: input.requestId, ...changed })),
|
|
64
|
+
).toEqual(new RequestConflict({ requestId: input.requestId }));
|
|
65
|
+
expect(yield* client.createNote(input)).toEqual(first);
|
|
66
|
+
expect(yield* client.listNotes()).toEqual({ notes: [{ id: first.id, title: "Plan" }] });
|
|
67
|
+
}).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
it.effect("scopes keys to the caller, and treats a new key as a new creation", () =>
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const client = yield* Action.client(notes);
|
|
73
|
+
const input = { requestId: "plan", title: "Plan", body: "Draft" };
|
|
74
|
+
const ada = yield* client.createNote(input).pipe(as("ada"));
|
|
75
|
+
const grace = yield* client.createNote({ ...input, body: "Grace's draft" }).pipe(as("grace"));
|
|
76
|
+
const another = yield* client.createNote({ ...input, requestId: "another" }).pipe(as("ada"));
|
|
77
|
+
|
|
78
|
+
expect(new Set([ada.id, grace.id, another.id]).size).toBe(3);
|
|
79
|
+
expect(yield* client.createNote(input).pipe(as("ada"))).toEqual(ada);
|
|
80
|
+
expect(yield* client.listNotes().pipe(as("ada"))).toEqual({
|
|
81
|
+
notes: [
|
|
82
|
+
{ id: ada.id, title: "Plan" },
|
|
83
|
+
{ id: another.id, title: "Plan" },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
expect(yield* client.listNotes().pipe(as("grace"))).toEqual({
|
|
87
|
+
notes: [{ id: grace.id, title: "Plan" }],
|
|
88
|
+
});
|
|
89
|
+
}).pipe(Effect.provide(NoteStore.layerMemory)),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it.effect("retains the original result after the note changes or is deleted", () =>
|
|
93
|
+
Effect.gen(function* () {
|
|
94
|
+
const client = yield* Action.client(notes);
|
|
95
|
+
const input = { requestId: "plan", title: "Plan", body: "Draft" };
|
|
96
|
+
const original = yield* client.createNote(input);
|
|
97
|
+
|
|
98
|
+
yield* client.updateNote({ id: original.id, body: "Final", revision: 1 });
|
|
99
|
+
|
|
100
|
+
expect(yield* client.createNote(input)).toEqual(original);
|
|
101
|
+
expect(yield* client.getNote({ id: original.id })).toMatchObject({
|
|
102
|
+
body: "Final",
|
|
103
|
+
revision: 2,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
yield* client.deleteNote({ id: original.id });
|
|
107
|
+
|
|
108
|
+
expect(yield* client.createNote(input)).toEqual(original);
|
|
109
|
+
expect(yield* client.listNotes()).toEqual({ notes: [] });
|
|
110
|
+
}).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
it.effect("creates one note for concurrent duplicates, including under frequent fiber yields", () =>
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
const client = yield* Action.client(notes);
|
|
116
|
+
const steps = Array.range(3, 60);
|
|
117
|
+
|
|
118
|
+
yield* Effect.forEach(steps, (limit) =>
|
|
119
|
+
Effect.gen(function* () {
|
|
120
|
+
const input = { requestId: `concurrent-${limit}`, title: "Plan", body: "Draft" };
|
|
121
|
+
|
|
122
|
+
const created = yield* Effect.all(Array.replicate(client.createNote(input), 3), {
|
|
123
|
+
concurrency: "unbounded",
|
|
124
|
+
}).pipe(Effect.provideService(References.MaxOpsBeforeYield, limit));
|
|
125
|
+
|
|
126
|
+
expect(created).toEqual(Array.replicate(created[0], 3));
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect((yield* client.listNotes()).notes).toHaveLength(steps.length);
|
|
131
|
+
}).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
|
|
132
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
2
2
|
import { Effect, Layer } from "effect";
|
|
3
|
-
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
+
import { HttpClient, HttpClientError, HttpClientRequest } from "effect/http";
|
|
4
4
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
5
5
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
6
6
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
@@ -84,4 +84,43 @@ describe("the service, over HTTP and MCP", () => {
|
|
|
84
84
|
expect(yield* response.text).toContain('"name":"createNote"');
|
|
85
85
|
}).pipe(Effect.provide(served)),
|
|
86
86
|
);
|
|
87
|
+
|
|
88
|
+
it.effect("retries a committed HTTP write over MCP after its response is lost", () =>
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
const http = yield* ActionHttp.client(Http, {
|
|
91
|
+
transformClient: (client) =>
|
|
92
|
+
client.pipe(
|
|
93
|
+
as("ada").transformClient,
|
|
94
|
+
// The real server finishes the write, but the caller receives no response.
|
|
95
|
+
HttpClient.transform((response, request) =>
|
|
96
|
+
response.pipe(
|
|
97
|
+
Effect.andThen(
|
|
98
|
+
Effect.fail(
|
|
99
|
+
new HttpClientError.HttpClientError({
|
|
100
|
+
reason: new HttpClientError.TransportError({
|
|
101
|
+
request,
|
|
102
|
+
description: "Response lost after commit",
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const mcp = yield* ActionMcp.client([GetStatus, ...Notes], as("ada"));
|
|
113
|
+
const input = { requestId: "plan", title: "Plan", body: "Draft" };
|
|
114
|
+
|
|
115
|
+
expect(yield* Effect.flip(http.createNote(input))).toBeInstanceOf(
|
|
116
|
+
HttpClientError.HttpClientError,
|
|
117
|
+
);
|
|
118
|
+
expect(yield* mcp.getStatus()).toEqual({ service: "notes-service", notes: 1 });
|
|
119
|
+
|
|
120
|
+
const replay = yield* mcp.createNote(input);
|
|
121
|
+
|
|
122
|
+
expect(replay).toMatchObject({ title: "Plan", body: "Draft", revision: 1 });
|
|
123
|
+
expect(yield* mcp.listNotes()).toEqual({ notes: [{ id: replay.id, title: "Plan" }] });
|
|
124
|
+
}).pipe(Effect.provide(served)),
|
|
125
|
+
);
|
|
87
126
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Define Effect action contracts once and project them onto HTTP, MCP, Effect RPC, native Toolkits, and CLIs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"@effect/platform-node": "4.0.4",
|
|
53
53
|
"@effect/vitest": "4.0.4",
|
|
54
54
|
"@modelcontextprotocol/client": "2.3.1",
|
|
55
|
-
"@types/node": "26.6.
|
|
55
|
+
"@types/node": "26.6.5",
|
|
56
56
|
"effect": "4.0.4",
|
|
57
57
|
"tsx": "4.23.15",
|
|
58
58
|
"typescript": "7.0.2",
|
|
59
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
60
|
-
"vite-plus": "1.
|
|
61
|
-
"vitest": "5.0.
|
|
59
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
|
|
60
|
+
"vite-plus": "1.2.0",
|
|
61
|
+
"vitest": "5.0.4"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"effect": "~4.0.4"
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region \0rolldown/runtime.js
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __exportAll = (all, no_symbols) => {
|
|
4
|
-
let target = {};
|
|
5
|
-
for (var name in all) __defProp(target, name, {
|
|
6
|
-
get: all[name],
|
|
7
|
-
enumerable: true
|
|
8
|
-
});
|
|
9
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
-
return target;
|
|
11
|
-
};
|
|
12
|
-
//#endregion
|
|
13
|
-
export { __exportAll };
|
package/dist/contract/rules.mjs
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { builtIns } from "./errors.mjs";
|
|
2
|
-
import { Predicate, Schema, SchemaAST } from "effect";
|
|
3
|
-
//#region src/contract/rules.ts
|
|
4
|
-
const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
|
|
5
|
-
const projectedErrors = (action, surface = []) => [.../* @__PURE__ */ new Set([
|
|
6
|
-
...builtIns,
|
|
7
|
-
...action.error,
|
|
8
|
-
...surface
|
|
9
|
-
])];
|
|
10
|
-
/**
|
|
11
|
-
* Every error a binding's call of `action` declares: the built-in ones, the action's, the
|
|
12
|
-
* binding's and, where `action` is protected, its authentication's.
|
|
13
|
-
*/
|
|
14
|
-
const bindingErrors = (action, binding) => projectedErrors(action, action.caller === Anyone || binding.authentication === void 0 ? binding.error : [...binding.error, ...binding.authentication.error]);
|
|
15
|
-
const assertAuthentication = (actions, authentication) => {
|
|
16
|
-
for (const action of actions) if (action.caller !== Anyone && (authentication === void 0 || action.caller.key !== authentication.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
20
|
-
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
21
|
-
* agree on a name.
|
|
22
|
-
*/
|
|
23
|
-
const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
|
|
24
|
-
const validName = /^[A-Za-z0-9_-]{1,128}$/;
|
|
25
|
-
const assertName = (what, name) => {
|
|
26
|
-
if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
|
|
27
|
-
};
|
|
28
|
-
const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
|
|
29
|
-
const seen = /* @__PURE__ */ new Map();
|
|
30
|
-
for (const item of items) {
|
|
31
|
-
const name = nameOf(item);
|
|
32
|
-
const claimant = claimantOf(item);
|
|
33
|
-
const other = seen.get(name);
|
|
34
|
-
if (other !== void 0) {
|
|
35
|
-
const by = other === claimant ? "" : `, claimed by ${other} and ${claimant}`;
|
|
36
|
-
throw new Error(`Duplicate ${what}: ${name}${by}`);
|
|
37
|
-
}
|
|
38
|
-
seen.set(name, claimant);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
const assertOnce = (what, actions) => assertDistinct(what, actions, (action) => action.name);
|
|
42
|
-
const assertKnown = (what, keys, names) => {
|
|
43
|
-
const unknown = keys.filter((key) => !names.includes(key));
|
|
44
|
-
if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")}`);
|
|
45
|
-
};
|
|
46
|
-
const unsuspended = (ast) => SchemaAST.isSuspend(ast) ? unsuspended(ast.thunk()) : ast;
|
|
47
|
-
const members = (ast) => {
|
|
48
|
-
const resolved = unsuspended(ast);
|
|
49
|
-
return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
|
|
50
|
-
};
|
|
51
|
-
const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
|
|
52
|
-
const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
|
|
53
|
-
const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
|
|
54
|
-
return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
|
|
55
|
-
});
|
|
56
|
-
const builtInTags = new Set(builtIns.flatMap((error) => encodedTags(error.ast)));
|
|
57
|
-
const assertOwnTags = (what, errors) => {
|
|
58
|
-
const tag = errors.flatMap((error) => encodedTags(error.ast)).find((tag) => builtInTags.has(tag));
|
|
59
|
-
if (tag !== void 0) throw new Error(`${what}: error _tag "${tag}" is built in, and declared on every surface`);
|
|
60
|
-
};
|
|
61
|
-
/**
|
|
62
|
-
* The checks `ActionHttp.make` and `ActionRpc.make` share: each action once, no built-in error in
|
|
63
|
-
* `error`, and the descriptor every protected action names. Returns `error` as a list.
|
|
64
|
-
*/
|
|
65
|
-
const bindingOf = (what, actions, options) => {
|
|
66
|
-
assertOnce("action", actions);
|
|
67
|
-
const error = errorList(options.error);
|
|
68
|
-
assertOwnTags(what, error);
|
|
69
|
-
assertAuthentication(actions, options.authentication);
|
|
70
|
-
return {
|
|
71
|
-
error,
|
|
72
|
-
authentication: options.authentication
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
|
|
76
|
-
const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
|
|
77
|
-
/**
|
|
78
|
-
* `error` as an endpoint declares it: each member of a union without a status of its own apart,
|
|
79
|
-
* each with its status, 422 without one.
|
|
80
|
-
*/
|
|
81
|
-
const declared = (error) => {
|
|
82
|
-
const status = statusThroughSuspensions(error.ast);
|
|
83
|
-
if (status !== void 0) return [httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: status }) : error];
|
|
84
|
-
const resolved = unsuspended(error.ast);
|
|
85
|
-
if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
|
|
86
|
-
return [error.annotate({ httpApiStatus: 422 })];
|
|
87
|
-
};
|
|
88
|
-
/**
|
|
89
|
-
* An action's MCP tool hints, MCP's defaults where its `mcp` leaves one out: a write destructive,
|
|
90
|
-
* a read not, no tool idempotent and every tool open-world. A given value passes through, as MCP
|
|
91
|
-
* calls `destructiveHint` and `idempotentHint` meaningless on a read-only tool.
|
|
92
|
-
*/
|
|
93
|
-
const hintsOf = ({ readOnly, mcp }) => ({
|
|
94
|
-
readOnlyHint: readOnly,
|
|
95
|
-
destructiveHint: mcp.destructiveHint ?? !readOnly,
|
|
96
|
-
idempotentHint: mcp.idempotentHint ?? false,
|
|
97
|
-
openWorldHint: mcp.openWorldHint ?? true
|
|
98
|
-
});
|
|
99
|
-
//#endregion
|
|
100
|
-
export { Anyone, assertAuthentication, assertDistinct, assertKnown, assertName, assertOnce, assertOwnTags, bindingErrors, bindingOf, declared, errorList, hintsOf, httpApiStatusOf, literalValues, members, projectedErrors, unsuspended };
|