@segmentstream-cloud/ontology 0.10.0 → 0.11.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.ts +19 -0
- package/dist/action.js +1 -0
- package/dist/analyze.d.ts +3 -1
- package/dist/analyze.js +81 -1
- package/dist/bin/migrate.js +3 -2
- package/dist/diagnostics.d.ts +3 -3
- package/dist/diagnostics.js +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -1
- package/dist/manifest.d.ts +48 -0
- package/dist/manifest.js +9 -0
- package/dist/migrate/apply.d.ts +7 -0
- package/dist/migrate/apply.js +18 -1
- package/dist/migrate/index.d.ts +1 -1
- package/dist/migrate/index.js +1 -1
- package/dist/migrate/model.d.ts +8 -0
- package/dist/migrate/plan.d.ts +6 -0
- package/dist/migrate/plan.js +10 -2
- package/dist/migrate/storage.d.ts +12 -0
- package/dist/migrate/storage.js +82 -0
- package/dist/names.d.ts +9 -1
- package/dist/names.js +11 -0
- package/dist/nitro.d.ts +13 -0
- package/dist/nitro.js +18 -0
- package/dist/runtime/generated-actions.d.ts +3 -0
- package/dist/runtime/generated-actions.js +3 -2
- package/dist/runtime/oauth.d.ts +9 -0
- package/dist/runtime/oauth.js +165 -0
- package/dist/runtime/principals.d.ts +5 -0
- package/dist/runtime/principals.js +15 -0
- package/dist/runtime/service-binding.js +2 -0
- package/dist/service/api.js +4 -10
- package/dist/service/authentication.d.ts +27 -1
- package/dist/service/authentication.js +48 -26
- package/dist/service/config.d.ts +5 -0
- package/dist/service/descriptions.d.ts +8 -0
- package/dist/service/descriptions.js +15 -0
- package/dist/service/errors.d.ts +5 -1
- package/dist/service/errors.js +6 -2
- package/dist/service/handlers.d.ts +1 -4
- package/dist/service/handlers.js +20 -203
- package/dist/service/mcp/server.d.ts +24 -0
- package/dist/service/mcp/server.js +370 -0
- package/dist/service/mcp/tools.d.ts +47 -0
- package/dist/service/mcp/tools.js +218 -0
- package/dist/service/oauth/protocol.d.ts +52 -0
- package/dist/service/oauth/protocol.js +110 -0
- package/dist/service/oauth/server.d.ts +27 -0
- package/dist/service/oauth/server.js +576 -0
- package/dist/service/operations.d.ts +114 -0
- package/dist/service/operations.js +218 -0
- package/dist/service/runtime-contract.d.ts +89 -0
- package/dist/service/schemas.d.ts +8 -0
- package/dist/service/service.d.ts +3 -3
- package/dist/service/service.js +10 -5
- package/package.json +6 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** `Requires lead:read.`, `Requires lead:create and lead:update.` */
|
|
2
|
+
export function requires(permissions) {
|
|
3
|
+
return `Requires ${permissions.join(" and ")}.`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* What an action's route and its MCP tool say about it: its description, whether it is generated,
|
|
7
|
+
* the permissions it requires and the connections it calls.
|
|
8
|
+
*/
|
|
9
|
+
export function actionDescription({ action, generated, permissions }) {
|
|
10
|
+
const origin = generated ? "Generated action." : "Action.";
|
|
11
|
+
const calls = action.connections.length > 0
|
|
12
|
+
? ` Calls ${action.connections.map((handle) => handle.key).join(", ")} before its transaction.`
|
|
13
|
+
: "";
|
|
14
|
+
return `${action.description}\n\n${origin} ${requires(permissions)}${calls}`;
|
|
15
|
+
}
|
package/dist/service/errors.d.ts
CHANGED
|
@@ -45,7 +45,11 @@ export declare function toServerResponse(response: ErrorResponse): HttpServerRes
|
|
|
45
45
|
export declare function errorBodyForWire(body: ErrorBody): Readonly<Record<string, unknown>>;
|
|
46
46
|
/** Issues of a schema failure, as `[{ path, message }]`. */
|
|
47
47
|
export declare function schemaIssues(issue: SchemaIssue.Issue): Array<ValidationIssue>;
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* `challenge` is the `WWW-Authenticate` value; the MCP server's names its OAuth metadata, which is
|
|
50
|
+
* how a client learns where members authorize it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function unauthorized(requestId: string, detail: string, challenge?: string): ErrorResponse;
|
|
49
53
|
/**
|
|
50
54
|
* The token is valid but the `invited` sign-in policy turns its holder away: nobody invited them.
|
|
51
55
|
* The detail is written for the person, since the app shows it.
|
package/dist/service/errors.js
CHANGED
|
@@ -50,11 +50,15 @@ const REQUEST_PARTS = {
|
|
|
50
50
|
function isJsonParseFailure(issue) {
|
|
51
51
|
return (issue._tag === "InvalidValue" && issue.annotations?.message === "Expected a valid JSON body");
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* `challenge` is the `WWW-Authenticate` value; the MCP server's names its OAuth metadata, which is
|
|
55
|
+
* how a client learns where members authorize it.
|
|
56
|
+
*/
|
|
57
|
+
export function unauthorized(requestId, detail, challenge = 'Bearer realm="ontology"') {
|
|
54
58
|
return {
|
|
55
59
|
status: 401,
|
|
56
60
|
body: { error: "unauthorized", detail, requestId },
|
|
57
|
-
headers: { "www-authenticate":
|
|
61
|
+
headers: { "www-authenticate": challenge },
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { type HttpApiGroup } from "effect/unstable/httpapi";
|
|
3
|
-
import type { Query } from "../query.js";
|
|
4
3
|
import { type BuiltApi } from "./api.js";
|
|
5
4
|
import type { AfterCommitHook } from "./config.js";
|
|
6
|
-
import {
|
|
5
|
+
import type { ConnectionTransportFactory } from "./connections.js";
|
|
7
6
|
import { type RateLimit } from "./rate-limit.js";
|
|
8
7
|
import { OntologyBackend } from "./runtime-contract.js";
|
|
9
|
-
/** The query the runtime receives: every key present only when set, and no empty `include`. */
|
|
10
|
-
export declare function toQuery(decoded: Query): Query;
|
|
11
8
|
/**
|
|
12
9
|
* Sign-in checks one client may make per minute. A person checks once per sign-in, a few times at
|
|
13
10
|
* most; the limit slows down anyone testing addresses to learn who the app's members are.
|
package/dist/service/handlers.js
CHANGED
|
@@ -3,19 +3,13 @@ import { Effect, Layer, Schema } from "effect";
|
|
|
3
3
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
4
4
|
import { STORAGE_GROUP } from "../storage/api.js";
|
|
5
5
|
import { Conflict } from "../errors.js";
|
|
6
|
-
import { editsFromWire, generatedResultFromWire, recordFromWire } from "../authoring-records.js";
|
|
7
|
-
import { nameToWire } from "../authoring-names.js";
|
|
8
|
-
import { principalFromWire } from "../authoring.js";
|
|
9
|
-
import { toCamelCase } from "../names.js";
|
|
10
|
-
import { PermissionDenied } from "../errors.js";
|
|
11
|
-
import { missingPermission, permissionOf, READ_VERB } from "../permissions.js";
|
|
12
6
|
import { EMAIL_PATTERN } from "../property.js";
|
|
13
7
|
import { renderCatalog } from "../render.js";
|
|
14
8
|
import { SERVICE_GROUP } from "./api.js";
|
|
15
9
|
import { CurrentPrincipal } from "./authentication.js";
|
|
16
10
|
import { catalogFor, graphOf } from "./catalog.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
11
|
+
import { InvalidRequestBody, SignInCheckLimited, schemaIssues } from "./errors.js";
|
|
12
|
+
import { identityToken, makeOperations, requireRead, toQuery, } from "./operations.js";
|
|
19
13
|
import { clientAddress, fixedWindowLimiter, } from "./rate-limit.js";
|
|
20
14
|
import { OntologyBackend } from "./runtime-contract.js";
|
|
21
15
|
import { ActionEnvelope } from "./schemas.js";
|
|
@@ -23,102 +17,6 @@ function dynamicGroup(api, identifier, build) {
|
|
|
23
17
|
const group = HttpApiBuilder.group;
|
|
24
18
|
return group(api, identifier, build);
|
|
25
19
|
}
|
|
26
|
-
/** The query the runtime receives: every key present only when set, and no empty `include`. */
|
|
27
|
-
export function toQuery(decoded) {
|
|
28
|
-
const { include, ...rest } = decoded;
|
|
29
|
-
return include !== undefined && Object.keys(include).length > 0 ? { ...rest, include } : rest;
|
|
30
|
-
}
|
|
31
|
-
function page(result) {
|
|
32
|
-
return { items: result.items, nextCursor: result.nextCursor ?? null };
|
|
33
|
-
}
|
|
34
|
-
function relationshipTargets(plan) {
|
|
35
|
-
return new Map(plan.entities.map((planned) => [
|
|
36
|
-
planned.name,
|
|
37
|
-
new Map(planned.traversals.map((traversal) => [traversal.name, traversal.target])),
|
|
38
|
-
]));
|
|
39
|
-
}
|
|
40
|
-
/** Adds the entities a `where` looks into through relationship filters, at any combinator depth. */
|
|
41
|
-
function filterReads(where, targets, reads) {
|
|
42
|
-
for (const [key, condition] of Object.entries(where ?? {})) {
|
|
43
|
-
if (key === "and" || key === "or") {
|
|
44
|
-
for (const part of condition) {
|
|
45
|
-
filterReads(part, targets, reads);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
else if (key === "not") {
|
|
49
|
-
filterReads(condition, targets, reads);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
const target = targets.get(key);
|
|
53
|
-
if (target !== undefined) {
|
|
54
|
-
reads.add(target);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The entities a query on `entity` reads: the entity, the entity of every relationship it includes,
|
|
61
|
-
* and the entity of every relationship its filter looks into.
|
|
62
|
-
*/
|
|
63
|
-
function queryReads(entity, query, targets) {
|
|
64
|
-
const own = targets.get(entity) ?? new Map();
|
|
65
|
-
const reads = new Set([entity]);
|
|
66
|
-
for (const name of Object.keys(query.include ?? {})) {
|
|
67
|
-
const target = own.get(name);
|
|
68
|
-
if (target !== undefined) {
|
|
69
|
-
reads.add(target);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
filterReads(query.where, own, reads);
|
|
73
|
-
return [...reads];
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Requires `<entity>:read` on every entity a request reads. A missing one answers 403 for the whole
|
|
77
|
-
* request, before anything is read: nothing is silently left out.
|
|
78
|
-
*/
|
|
79
|
-
function requireRead(entities) {
|
|
80
|
-
return Effect.gen(function* () {
|
|
81
|
-
const principal = yield* CurrentPrincipal;
|
|
82
|
-
const missing = missingPermission(entities.map((entity) => permissionOf(entity, READ_VERB)), principal);
|
|
83
|
-
if (missing !== undefined) {
|
|
84
|
-
return yield* new PermissionDenied({
|
|
85
|
-
permission: missing,
|
|
86
|
-
message: `Reading requires ${missing}, which the principal's roles do not grant.`,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Follows a relationship from one record with the query language: the record's own
|
|
93
|
-
* `<relationship>_id` for a `rel.one`, and for everything to-many a relationship path from the
|
|
94
|
-
* target back to the record (`lead.notes` is notes whose `lead` is the lead, `lead.tags` is tags
|
|
95
|
-
* some of whose `leads` is the lead), combined with the caller's query and paged like any find.
|
|
96
|
-
*/
|
|
97
|
-
function traverse(planned, traversal, request, targets) {
|
|
98
|
-
return Effect.gen(function* () {
|
|
99
|
-
const backend = yield* OntologyBackend;
|
|
100
|
-
const { id } = request.params;
|
|
101
|
-
const query = toQuery(request.query);
|
|
102
|
-
yield* requireRead([planned.name, ...queryReads(traversal.target, query, targets)]);
|
|
103
|
-
const source = yield* backend.get({ entity: planned.name, id });
|
|
104
|
-
if (traversal.kind === "one") {
|
|
105
|
-
const targetId = source[`${traversal.name}_id`];
|
|
106
|
-
if (typeof targetId !== "string") {
|
|
107
|
-
return { items: [], nextCursor: null };
|
|
108
|
-
}
|
|
109
|
-
return page(yield* backend.find({
|
|
110
|
-
entity: traversal.target,
|
|
111
|
-
query: { ...query, where: { id: { eq: targetId } }, limit: 1 },
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
const match = { id: { eq: id } };
|
|
115
|
-
const related = traversal.kind === "inverseOfOne"
|
|
116
|
-
? { [traversal.opposite]: match }
|
|
117
|
-
: { [traversal.opposite]: { some: match } };
|
|
118
|
-
const where = query.where === undefined ? related : { and: [related, query.where] };
|
|
119
|
-
return page(yield* backend.find({ entity: traversal.target, query: { ...query, where } }));
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
20
|
function isJsonMediaType(contentType) {
|
|
123
21
|
if (contentType === undefined || contentType.trim() === "") {
|
|
124
22
|
return true;
|
|
@@ -143,68 +41,7 @@ function readEnvelope(request) {
|
|
|
143
41
|
return yield* Schema.decodeUnknownEffect(ActionEnvelope)(body, { errors: "all" }).pipe(Effect.mapError((error) => new InvalidRequestBody({ reason: "invalid_request", issues: schemaIssues(error.issue) })));
|
|
144
42
|
});
|
|
145
43
|
}
|
|
146
|
-
|
|
147
|
-
* The configured hook as `runAction` runs it after the commit. A failure is logged with the request
|
|
148
|
-
* id here, so the action's caller still gets the committed result.
|
|
149
|
-
*/
|
|
150
|
-
function afterCommitOf(hook, request, ontology) {
|
|
151
|
-
return (committed) => {
|
|
152
|
-
const definition = ontology.config.entities.find((entity) => nameToWire(entity.name) === committed.entity)?.definition;
|
|
153
|
-
return Effect.tryPromise(async () => hook({
|
|
154
|
-
entity: toCamelCase(committed.entity),
|
|
155
|
-
action: toCamelCase(committed.action),
|
|
156
|
-
principal: principalFromWire(committed.principal),
|
|
157
|
-
input: request.generated ? recordFromWire(committed.input, definition) : committed.input,
|
|
158
|
-
result: request.generated
|
|
159
|
-
? generatedResultFromWire(committed.result, committed.action, definition)
|
|
160
|
-
: committed.result,
|
|
161
|
-
edits: editsFromWire(committed.edits, ontology.config.entities),
|
|
162
|
-
requestId: request.requestId,
|
|
163
|
-
})).pipe(Effect.catchCause((cause) => Effect.logError("ontology.after_commit.failed", cause).pipe(Effect.annotateLogs({
|
|
164
|
-
requestId: request.requestId,
|
|
165
|
-
action: `${committed.entity}.${committed.action}`,
|
|
166
|
-
}))));
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
/** The deployment's OIDC token: Vercel sets it on each request, and in the environment of builds. */
|
|
170
|
-
function identityToken(request) {
|
|
171
|
-
const header = request.headers[OIDC_TOKEN_HEADER];
|
|
172
|
-
return typeof header === "string" && header !== "" ? header : process.env.VERCEL_OIDC_TOKEN;
|
|
173
|
-
}
|
|
174
|
-
function runAction(planned, { name, generated }, ontology, hook, connections) {
|
|
175
|
-
return ({ request }) => Effect.gen(function* () {
|
|
176
|
-
const backend = yield* OntologyBackend;
|
|
177
|
-
const principal = yield* CurrentPrincipal;
|
|
178
|
-
const requestId = yield* RequestId;
|
|
179
|
-
const envelope = yield* readEnvelope(request);
|
|
180
|
-
const validateOnly = envelope.options?.validateOnly === true;
|
|
181
|
-
const outcome = yield* backend.runAction({
|
|
182
|
-
entity: planned.name,
|
|
183
|
-
action: name,
|
|
184
|
-
input: envelope.input,
|
|
185
|
-
principal,
|
|
186
|
-
options: { ...envelope.options, validateOnly },
|
|
187
|
-
requestId,
|
|
188
|
-
connections: connections({
|
|
189
|
-
identityToken: identityToken(request),
|
|
190
|
-
audit: { action: `${planned.name}.${name}`, principalId: principal.id, requestId },
|
|
191
|
-
}),
|
|
192
|
-
...(hook === undefined
|
|
193
|
-
? {}
|
|
194
|
-
: { afterCommit: afterCommitOf(hook, { requestId, generated }, ontology) }),
|
|
195
|
-
});
|
|
196
|
-
const { issues } = outcome.validation;
|
|
197
|
-
if (!outcome.validation.ok && !validateOnly) {
|
|
198
|
-
return yield* new InvalidActionInput({ issues });
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
result: outcome.result ?? null,
|
|
202
|
-
validation: { ok: outcome.validation.ok, issues },
|
|
203
|
-
edits: outcome.edits,
|
|
204
|
-
};
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
function entityHandlers(built, planned, hooks, targets, connections) {
|
|
44
|
+
function entityHandlers(built, planned, operationsOf) {
|
|
208
45
|
return dynamicGroup(built.api, planned.name, (handlers) => Effect.gen(function* () {
|
|
209
46
|
if (planned.resolved.entity.definition.source !== undefined) {
|
|
210
47
|
return handlers.handleRaw("sourceCollection", () => requireRead([planned.name]).pipe(Effect.andThen(() => HttpServerResponse.json({
|
|
@@ -213,14 +50,12 @@ function entityHandlers(built, planned, hooks, targets, connections) {
|
|
|
213
50
|
}, { status: 404 }))));
|
|
214
51
|
}
|
|
215
52
|
const backend = yield* OntologyBackend;
|
|
216
|
-
|
|
217
|
-
const find = (query) => requireRead(queryReads(planned.name, query, targets)).pipe(Effect.andThen(() => backend.find({ entity: planned.name, query })), Effect.map(page));
|
|
218
|
-
const readOwn = requireRead([planned.name]);
|
|
53
|
+
const operations = operationsOf(backend);
|
|
219
54
|
let registered = handlers
|
|
220
|
-
.handle("find", (request) => find(toQuery(request.query)))
|
|
221
|
-
.handle("query", (request) => find(toQuery(request.payload)))
|
|
222
|
-
.handle("get", (request) =>
|
|
223
|
-
.handle("history", (request) =>
|
|
55
|
+
.handle("find", (request) => operations.find(planned, toQuery(request.query)))
|
|
56
|
+
.handle("query", (request) => operations.find(planned, toQuery(request.payload)))
|
|
57
|
+
.handle("get", (request) => operations.get(planned, request.params.id))
|
|
58
|
+
.handle("history", (request) => operations.history(planned, request.params.id, request.query));
|
|
224
59
|
for (const property of planned.resolved.properties.filter((property) => property.definition.type === "blob")) {
|
|
225
60
|
registered = registered.handle(`blobs.${property.name}`, (request) => Effect.gen(function* () {
|
|
226
61
|
const principal = yield* CurrentPrincipal;
|
|
@@ -234,37 +69,19 @@ function entityHandlers(built, planned, hooks, targets, connections) {
|
|
|
234
69
|
}));
|
|
235
70
|
}
|
|
236
71
|
for (const traversal of planned.traversals) {
|
|
237
|
-
registered = registered.handle(`related.${traversal.name}`, (request) => traverse(planned, traversal, request,
|
|
72
|
+
registered = registered.handle(`related.${traversal.name}`, (request) => operations.traverse(planned, traversal, request.params.id, toQuery(request.query)));
|
|
238
73
|
}
|
|
239
74
|
for (const action of planned.actions) {
|
|
240
|
-
|
|
241
|
-
|
|
75
|
+
registered = registered.handleRaw(`actions.${action.name}`, ({ request }) => readEnvelope(request).pipe(Effect.flatMap((envelope) => operations.runAction(planned, action, {
|
|
76
|
+
input: envelope.input,
|
|
77
|
+
validateOnly: envelope.options?.validateOnly === true,
|
|
78
|
+
...(envelope.options === undefined ? {} : { options: envelope.options }),
|
|
79
|
+
identityToken: identityToken(request),
|
|
80
|
+
}))));
|
|
242
81
|
}
|
|
243
82
|
return registered;
|
|
244
83
|
}));
|
|
245
84
|
}
|
|
246
|
-
/**
|
|
247
|
-
* The caller, as `GET /api/ontology/me` answers: their user record and the roles and permissions
|
|
248
|
-
* authentication resolved. It needs no permission: anyone may learn who they are and what they may do.
|
|
249
|
-
*/
|
|
250
|
-
function callerOf(backend) {
|
|
251
|
-
return Effect.gen(function* () {
|
|
252
|
-
const principal = yield* CurrentPrincipal;
|
|
253
|
-
// Authentication resolved the principal from this user a moment ago; its absence is a defect.
|
|
254
|
-
const user = yield* backend.get({ entity: "user", id: principal.id }).pipe(Effect.orDie);
|
|
255
|
-
return {
|
|
256
|
-
user: {
|
|
257
|
-
id: principal.id,
|
|
258
|
-
name: String(user.name),
|
|
259
|
-
email: typeof user.email === "string" ? user.email : null,
|
|
260
|
-
kind: principal.kind,
|
|
261
|
-
active: user.active === true,
|
|
262
|
-
},
|
|
263
|
-
roles: principal.roles,
|
|
264
|
-
permissions: principal.permissions,
|
|
265
|
-
};
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
85
|
/**
|
|
269
86
|
* Sign-in checks one client may make per minute. A person checks once per sign-in, a few times at
|
|
270
87
|
* most; the limit slows down anyone testing addresses to learn who the app's members are.
|
|
@@ -291,7 +108,7 @@ function checkSignIn(backend, limiter) {
|
|
|
291
108
|
return yield* backend.checkSignIn(email);
|
|
292
109
|
});
|
|
293
110
|
}
|
|
294
|
-
function serviceHandlers(built) {
|
|
111
|
+
function serviceHandlers(built, operationsOf) {
|
|
295
112
|
const { manifest } = built.plan.ontology;
|
|
296
113
|
const graph = graphOf(manifest);
|
|
297
114
|
const signInLimiter = fixedWindowLimiter(SIGN_IN_CHECK_LIMIT);
|
|
@@ -309,7 +126,7 @@ function serviceHandlers(built) {
|
|
|
309
126
|
return handlers
|
|
310
127
|
.handle("catalog", (request) => Effect.map(permissionsFor(request.query.as), (permissions) => catalogFor(manifest, permissions)))
|
|
311
128
|
.handle("graph", () => Effect.succeed(graph))
|
|
312
|
-
.handle("me", () =>
|
|
129
|
+
.handle("me", () => operationsOf(backend).caller)
|
|
313
130
|
.handle("openapi", () => Effect.sync(() => {
|
|
314
131
|
if (openApi === undefined) {
|
|
315
132
|
openApi = OpenApi.fromApi(built.api);
|
|
@@ -362,13 +179,13 @@ function storageHandlers(built) {
|
|
|
362
179
|
}
|
|
363
180
|
/** The handlers of every group of the API; they require {@link OntologyBackend}. */
|
|
364
181
|
export function handlersLayer(built, hooks, connections) {
|
|
365
|
-
const
|
|
182
|
+
const operationsOf = (backend) => makeOperations(backend, built.plan, hooks, connections);
|
|
366
183
|
const layers = [
|
|
367
|
-
serviceHandlers(built),
|
|
184
|
+
serviceHandlers(built, operationsOf),
|
|
368
185
|
...(built.plan.ontology.manifest.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))
|
|
369
186
|
? [storageHandlers(built)]
|
|
370
187
|
: []),
|
|
371
|
-
...built.plan.entities.map((planned) => entityHandlers(built, planned,
|
|
188
|
+
...built.plan.entities.map((planned) => entityHandlers(built, planned, operationsOf)),
|
|
372
189
|
];
|
|
373
190
|
const [first, ...rest] = layers;
|
|
374
191
|
return Layer.mergeAll(first, ...rest);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
3
|
+
import type { BuiltApi } from "../api.js";
|
|
4
|
+
import { type TokenVerifier } from "../authentication.js";
|
|
5
|
+
import type { AfterCommitHook } from "../config.js";
|
|
6
|
+
import type { ConnectionTransportFactory } from "../connections.js";
|
|
7
|
+
import { RequestId } from "../errors.js";
|
|
8
|
+
import { OntologyBackend } from "../runtime-contract.js";
|
|
9
|
+
/** The URL path of the ontology's MCP server, outside `/api/ontology`: one per app. */
|
|
10
|
+
export declare const MCP_PATH = "/mcp";
|
|
11
|
+
/** The MCP revisions the server speaks, newest first. */
|
|
12
|
+
export declare const MCP_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18"];
|
|
13
|
+
export interface McpServerOptions {
|
|
14
|
+
readonly built: BuiltApi;
|
|
15
|
+
readonly hooks: ReadonlyMap<string, AfterCommitHook>;
|
|
16
|
+
readonly connections: ConnectionTransportFactory;
|
|
17
|
+
readonly verifyToken: TokenVerifier;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The ontology's MCP server at `POST /mcp`: Streamable HTTP without sessions, so any instance can
|
|
21
|
+
* answer any request. Its tools run the same operations as the HTTP routes, behind the same
|
|
22
|
+
* authentication. `GET` and `DELETE`, which open a server stream and end a session, answer 405.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mcpLayer(options: McpServerOptions): Layer.Layer<never, never, OntologyBackend | HttpRouter.HttpRouter | HttpRouter.Request<"Requires", RequestId>>;
|