agent-cms 0.1.0 → 0.3.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 +113 -2
- package/dist/codemode-handler-Bgu2utjN.mjs +18697 -0
- package/dist/{handler-ClOW1ldA.mjs → handler-B5jgfPOY.mjs} +2 -2
- package/dist/http-transport-DVKhbbe1.mjs +17 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +431 -34
- package/dist/{token-service-BDjccMmz.mjs → preview-service-C9Tmhdye.mjs} +177 -38
- package/dist/{http-transport-DbFCI6Cs.mjs → server-D0XqvDjU.mjs} +200 -241
- package/dist/{structured-text-service-B4xSlUg_.mjs → structured-text-service-BJkqWRkq.mjs} +187 -8
- package/migrations/0000_genesis.sql +11 -0
- package/package.json +5 -5
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { B as encodeJson, J as ValidationError, K as SchemaEngineError, L as decodeJsonRecordStringOr
|
|
1
|
+
import { $ as bulkCreateRecords, A as clearSchedule, B as AssetImportContext, C as ReorderInput, Ct as search, E as SearchInput, G as listAssets, K as replaceAsset, L as removeBlockFromWhitelist, M as schedulePublish, N as scheduleUnpublish, Q as unpublishRecord, R as removeBlockType, S as ReindexSearchInput, St as reindexAll, V as createAsset, W as importAssetFromUrl, X as bulkUnpublishRecords, Y as bulkPublishRecords, Z as publishRecord, _t as deleteModel, a as listEditorTokens, at as removeRecord, b as PatchBlocksInput, c as exportSchema, ct as getVersion, d as CreateAssetInput, dt as HooksContext, et as createRecord, ft as createField, g as CreateRecordInput, gt as createModel, h as CreateModelInput, ht as updateField, i as createEditorToken, it as patchRecord, l as importSchema, lt as listVersions, n as resolvePreviewPath, nt as listRecords, o as revokeEditorToken, ot as reorderRecords, p as CreateFieldInput, pt as deleteField, rt as patchBlocksForField, st as updateSingletonRecord, t as createPreviewToken, tt as getRecord, ut as restoreVersion, v as ImportAssetFromUrlInput, wt as VectorizeContext, xt as updateModel, y as ImportSchemaInput, yt as getModelByApiKey, z as removeLocale } from "./preview-service-C9Tmhdye.mjs";
|
|
2
|
+
import { B as encodeJson, J as ValidationError, K as SchemaEngineError, L as decodeJsonRecordStringOr } from "./structured-text-service-BJkqWRkq.mjs";
|
|
3
3
|
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
4
4
|
import { SqlClient } from "@effect/sql";
|
|
5
|
-
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter";
|
|
6
5
|
import * as McpServer from "@effect/ai/McpServer";
|
|
7
6
|
import * as McpSchema from "@effect/ai/McpSchema";
|
|
8
7
|
import * as AiTool from "@effect/ai/Tool";
|
|
@@ -72,37 +71,6 @@ const CommonDependencies = [
|
|
|
72
71
|
HooksContext,
|
|
73
72
|
AssetImportContext
|
|
74
73
|
];
|
|
75
|
-
const BlockEntry = Schema.Struct({
|
|
76
|
-
id: Schema.String,
|
|
77
|
-
type: Schema.String,
|
|
78
|
-
data: JsonRecord
|
|
79
|
-
});
|
|
80
|
-
const BuildStructuredTextInput = Schema.Struct({
|
|
81
|
-
markdown: Schema.optional(Schema.String),
|
|
82
|
-
blocks: Schema.optional(Schema.Array(BlockEntry)),
|
|
83
|
-
nodes: Schema.optional(Schema.Array(Schema.Union(Schema.Struct({
|
|
84
|
-
type: Schema.Literal("paragraph"),
|
|
85
|
-
text: Schema.String
|
|
86
|
-
}), Schema.Struct({
|
|
87
|
-
type: Schema.Literal("heading"),
|
|
88
|
-
level: Schema.Number,
|
|
89
|
-
text: Schema.String
|
|
90
|
-
}), Schema.Struct({
|
|
91
|
-
type: Schema.Literal("code"),
|
|
92
|
-
code: Schema.String,
|
|
93
|
-
language: Schema.optional(Schema.String)
|
|
94
|
-
}), Schema.Struct({
|
|
95
|
-
type: Schema.Literal("blockquote"),
|
|
96
|
-
text: Schema.String
|
|
97
|
-
}), Schema.Struct({
|
|
98
|
-
type: Schema.Literal("list"),
|
|
99
|
-
style: Schema.optional(Schema.Literal("bulleted", "numbered")),
|
|
100
|
-
items: Schema.Array(Schema.String)
|
|
101
|
-
}), Schema.Struct({ type: Schema.Literal("thematicBreak") }), Schema.Struct({
|
|
102
|
-
type: Schema.Literal("block"),
|
|
103
|
-
ref: Schema.String
|
|
104
|
-
}))))
|
|
105
|
-
});
|
|
106
74
|
const UpdateModelInput = Schema.Struct({
|
|
107
75
|
modelId: Schema.String,
|
|
108
76
|
name: Schema.optional(Schema.String),
|
|
@@ -110,7 +78,8 @@ const UpdateModelInput = Schema.Struct({
|
|
|
110
78
|
singleton: Schema.optional(Schema.Boolean),
|
|
111
79
|
sortable: Schema.optional(Schema.Boolean),
|
|
112
80
|
hasDraft: Schema.optional(Schema.Boolean),
|
|
113
|
-
allLocalesRequired: Schema.optional(Schema.Boolean)
|
|
81
|
+
allLocalesRequired: Schema.optional(Schema.Boolean),
|
|
82
|
+
canonicalPathTemplate: Schema.optional(Schema.NullOr(Schema.String))
|
|
114
83
|
});
|
|
115
84
|
const UpdateFieldInput = Schema.Struct({
|
|
116
85
|
fieldId: Schema.String,
|
|
@@ -132,16 +101,6 @@ const UpdateRecordInput = Schema.Struct({
|
|
|
132
101
|
modelApiKey: Schema.String,
|
|
133
102
|
data: Schema.optionalWith(JsonRecord, { default: () => ({}) })
|
|
134
103
|
});
|
|
135
|
-
const PatchBlocksInput = Schema.Struct({
|
|
136
|
-
recordId: Schema.String,
|
|
137
|
-
modelApiKey: Schema.String,
|
|
138
|
-
fieldApiKey: Schema.String,
|
|
139
|
-
value: Schema.optional(Schema.Unknown),
|
|
140
|
-
blocks: Schema.Record({
|
|
141
|
-
key: Schema.String,
|
|
142
|
-
value: Schema.NullOr(Schema.Unknown)
|
|
143
|
-
})
|
|
144
|
-
});
|
|
145
104
|
const DeleteRecordInput = Schema.Struct({
|
|
146
105
|
recordId: Schema.String,
|
|
147
106
|
modelApiKey: Schema.String
|
|
@@ -155,6 +114,10 @@ const PublishRecordsInput = Schema.Struct({
|
|
|
155
114
|
modelApiKey: Schema.String,
|
|
156
115
|
recordIds: Schema.Array(Schema.String).pipe(Schema.filter((value) => value.length >= 1, { message: () => "recordIds must contain at least 1 entry" }), Schema.filter((value) => value.length <= 1e3, { message: () => "recordIds must contain at most 1000 entries" }))
|
|
157
116
|
});
|
|
117
|
+
const SetPublishStatusInput = Schema.Struct({
|
|
118
|
+
action: Schema.Literal("publish", "unpublish"),
|
|
119
|
+
...PublishRecordsInput.fields
|
|
120
|
+
});
|
|
158
121
|
const ScheduleInput = Schema.Struct({
|
|
159
122
|
recordId: Schema.String,
|
|
160
123
|
modelApiKey: Schema.String,
|
|
@@ -201,6 +164,10 @@ const GetRecordInput = Schema.Struct({
|
|
|
201
164
|
recordId: Schema.String,
|
|
202
165
|
modelApiKey: Schema.String
|
|
203
166
|
});
|
|
167
|
+
const GetPreviewUrlInput = Schema.Struct({
|
|
168
|
+
recordId: Schema.String,
|
|
169
|
+
modelApiKey: Schema.String
|
|
170
|
+
});
|
|
204
171
|
const SetupContentModelPromptInput = Schema.Struct({ description: Schema.String });
|
|
205
172
|
const GenerateGraphqlQueriesPromptInput = Schema.Struct({ modelApiKey: Schema.String });
|
|
206
173
|
function cmsTool(name, description, parameters) {
|
|
@@ -211,48 +178,60 @@ function cmsTool(name, description, parameters) {
|
|
|
211
178
|
failure: Schema.Unknown,
|
|
212
179
|
dependencies: CommonDependencies
|
|
213
180
|
});
|
|
214
|
-
const isReadonly = name.startsWith("query_") || name.startsWith("get_") || name === "schema_info" || name === "
|
|
181
|
+
const isReadonly = name.startsWith("query_") || name.startsWith("get_") || name === "schema_info" || name === "search_content";
|
|
215
182
|
tool = tool.annotate(AiTool.Readonly, isReadonly);
|
|
216
183
|
tool = tool.annotate(AiTool.Idempotent, isReadonly || name.startsWith("update_") || name.startsWith("replace_"));
|
|
217
184
|
tool = tool.annotate(AiTool.Destructive, name.startsWith("delete_") || name === "remove_block");
|
|
218
185
|
tool = tool.annotate(AiTool.OpenWorld, name === "search_content");
|
|
219
186
|
return tool;
|
|
220
187
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Parse a text string with inline markdown into DAST inline (span) nodes.
|
|
223
|
-
* Returns the children of the first paragraph, or a single span fallback.
|
|
224
|
-
*/
|
|
225
|
-
function parseInlineSpans(text) {
|
|
226
|
-
const first = markdownToDast(text).document.children.at(0);
|
|
227
|
-
if (first != null && "children" in first) return first.children;
|
|
228
|
-
return [{
|
|
229
|
-
type: "span",
|
|
230
|
-
value: text
|
|
231
|
-
}];
|
|
232
|
-
}
|
|
233
|
-
function collectBlockRefs(node, refs = /* @__PURE__ */ new Set()) {
|
|
234
|
-
if (node == null || typeof node !== "object") return refs;
|
|
235
|
-
if (Array.isArray(node)) {
|
|
236
|
-
for (const entry of node) collectBlockRefs(entry, refs);
|
|
237
|
-
return refs;
|
|
238
|
-
}
|
|
239
|
-
if ("type" in node && node.type === "block" && "item" in node && typeof node.item === "string") refs.add(node.item);
|
|
240
|
-
for (const value of Object.values(node)) collectBlockRefs(value, refs);
|
|
241
|
-
return refs;
|
|
242
|
-
}
|
|
243
|
-
function assertKnownBlockRefs(blockMap, refs) {
|
|
244
|
-
for (const ref of refs) if (!(ref in blockMap)) throw new Error(`Unknown block ref '${ref}'. Define it in blocks before referencing it.`);
|
|
245
|
-
}
|
|
246
|
-
function assertNoUnusedBlocks(blockMap, refs) {
|
|
247
|
-
const unused = Object.keys(blockMap).filter((id) => !refs.has(id));
|
|
248
|
-
if (unused.length > 0) throw new Error(`Unused blocks: ${unused.join(", ")}. Every block must be referenced in the document.`);
|
|
249
|
-
}
|
|
250
188
|
function toStructuredContent(value) {
|
|
251
189
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
252
190
|
}
|
|
253
191
|
function isToolPayload(value) {
|
|
254
192
|
return value !== null && typeof value === "object";
|
|
255
193
|
}
|
|
194
|
+
function compactPatchBlocksResponse(fullRecord, fieldApiKey, deletedBlockIds) {
|
|
195
|
+
const fieldValue = fullRecord[fieldApiKey];
|
|
196
|
+
const envelope = (() => {
|
|
197
|
+
if (fieldValue === null || fieldValue === void 0) return null;
|
|
198
|
+
if (typeof fieldValue === "string") try {
|
|
199
|
+
return JSON.parse(fieldValue);
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (typeof fieldValue === "object" && !Array.isArray(fieldValue)) return fieldValue;
|
|
204
|
+
return null;
|
|
205
|
+
})();
|
|
206
|
+
if (!envelope) return {
|
|
207
|
+
recordId: fullRecord.id,
|
|
208
|
+
status: fullRecord._status ?? null,
|
|
209
|
+
fieldApiKey,
|
|
210
|
+
field: null,
|
|
211
|
+
blocks: {},
|
|
212
|
+
deleted: deletedBlockIds,
|
|
213
|
+
blockOrder: []
|
|
214
|
+
};
|
|
215
|
+
const blocks = typeof envelope.blocks === "object" && envelope.blocks !== null && !Array.isArray(envelope.blocks) ? envelope.blocks : {};
|
|
216
|
+
const blockOrder = [];
|
|
217
|
+
function walkDast(node) {
|
|
218
|
+
if (typeof node !== "object" || node === null) return;
|
|
219
|
+
const n = node;
|
|
220
|
+
if (n.type === "block" && typeof n.item === "string") blockOrder.push(n.item);
|
|
221
|
+
if (Array.isArray(n.children)) n.children.forEach(walkDast);
|
|
222
|
+
}
|
|
223
|
+
const value = envelope.value;
|
|
224
|
+
if (typeof value === "object" && value !== null) walkDast(value.document);
|
|
225
|
+
return {
|
|
226
|
+
recordId: fullRecord.id,
|
|
227
|
+
status: fullRecord._status ?? null,
|
|
228
|
+
fieldApiKey,
|
|
229
|
+
field: envelope,
|
|
230
|
+
blocks,
|
|
231
|
+
deleted: deletedBlockIds,
|
|
232
|
+
blockOrder
|
|
233
|
+
};
|
|
234
|
+
}
|
|
256
235
|
function parseValidators(value) {
|
|
257
236
|
if (value == null || value === "") return {};
|
|
258
237
|
if (typeof value === "string") return decodeJsonRecordStringOr(value, {});
|
|
@@ -297,10 +276,10 @@ Key validators by field type:
|
|
|
297
276
|
const UpdateFieldTool = cmsTool("update_field", "Update field properties (label, apiKey, validators, hint)", UpdateFieldInput.fields);
|
|
298
277
|
const DeleteModelTool = cmsTool("delete_model", "Delete a model (fails if referenced)", ModelIdInput.fields);
|
|
299
278
|
const DeleteFieldTool = cmsTool("delete_field", "Delete a field and drop column", FieldIdInput.fields);
|
|
300
|
-
const CreateRecordTool = cmsTool("create_record", `Create a content record. Records on draft-enabled models start as draft — call
|
|
279
|
+
const CreateRecordTool = cmsTool("create_record", `Create a content record. Records on draft-enabled models start as draft — call set_publish_status to make them visible in GraphQL.
|
|
301
280
|
|
|
302
281
|
Validation note:
|
|
303
|
-
- For models with drafts (has_draft=true), required-field validation is deferred until
|
|
282
|
+
- For models with drafts (has_draft=true), required-field validation is deferred until set_publish_status.
|
|
304
283
|
- For models without drafts (has_draft=false), required fields are enforced during create_record.
|
|
305
284
|
|
|
306
285
|
Field value formats:
|
|
@@ -309,10 +288,20 @@ Field value formats:
|
|
|
309
288
|
- link: record ID string
|
|
310
289
|
- links: array of record ID strings
|
|
311
290
|
- seo: {"title":"...","description":"...","image":"<asset_id>","twitterCard":"summary_large_image"}
|
|
312
|
-
- structured_text: {"value":{"schema":"dast","document":{...}},"blocks":{
|
|
291
|
+
- structured_text: markdown string, typed nodes array, {markdown:"...",blocks:[...]}, {nodes:[...],blocks:[...]}, or full DAST envelope {"value":{"schema":"dast","document":{...}},"blocks":{...}}
|
|
292
|
+
Markdown mode is the easiest way to write prose. Standard inline formatting (**bold**, *italic*, \`code\`, ~~strikethrough~~, [links](url)) all work.
|
|
293
|
+
Special sentinels for CMS references in markdown:
|
|
294
|
+
- Block refs: <!-- cms:block:BLOCK_ID --> (on its own line)
|
|
295
|
+
- Inline items: <!-- cms:inlineItem:RECORD_ID -->
|
|
296
|
+
- Inline blocks: <!-- cms:inlineBlock:BLOCK_ID -->
|
|
297
|
+
- Record links: [link text](itemLink:RECORD_ID)
|
|
298
|
+
When using {markdown, blocks}, the blocks array/map provides block data and sentinels place them in the document.
|
|
299
|
+
Those block field values are persisted by the same create_record/update_record call — you do not need a follow-up patch_blocks call just to save the initial block payload.
|
|
313
300
|
- color: {"red":255,"green":0,"blue":0,"alpha":255}
|
|
314
301
|
- lat_lon: {"latitude":64.13,"longitude":-21.89}`, CreateRecordInput.fields);
|
|
315
|
-
const UpdateRecordTool = cmsTool("update_record",
|
|
302
|
+
const UpdateRecordTool = cmsTool("update_record", `Update record fields. For singletons, recordId can be omitted — the single record is found automatically.
|
|
303
|
+
|
|
304
|
+
Accepts all the same field value formats as create_record, including markdown mode for structured_text. For editorial content edits, prefer markdown mode over hand-assembled DAST — it's simpler and less error-prone.`, UpdateRecordInput.fields);
|
|
316
305
|
const PatchBlocksTool = cmsTool("patch_blocks", `Partially update blocks in a structured text field without resending the entire content tree.
|
|
317
306
|
|
|
318
307
|
You can target block IDs from either:
|
|
@@ -320,26 +309,34 @@ You can target block IDs from either:
|
|
|
320
309
|
- nested structured_text sub-fields stored inside those blocks
|
|
321
310
|
|
|
322
311
|
Patch map semantics for each block ID:
|
|
323
|
-
- string value (the block ID) → keep block unchanged
|
|
324
312
|
- object with field overrides → merge into existing block (only specified fields updated)
|
|
325
313
|
- null → delete block and auto-prune from the relevant DAST tree
|
|
326
314
|
|
|
327
|
-
Block IDs not in the patch map are kept unchanged.
|
|
315
|
+
Block IDs not in the patch map are kept unchanged. Omit a block ID to leave it as-is.
|
|
328
316
|
|
|
329
317
|
If a nested block ID appears in multiple nested structured_text locations, the tool will fail and ask you to patch the parent block explicitly.
|
|
330
318
|
|
|
331
319
|
Optionally provide a new top-level DAST \`value\`. If omitted, the existing DAST is preserved (with deleted top-level blocks auto-pruned).
|
|
332
320
|
|
|
321
|
+
Use \`append\` to insert new blocks without reconstructing the DAST. Each entry is a record with \`_type\` and field values. New block IDs are auto-generated, DAST block nodes are appended to the end, and the response includes \`_appendedIds\`. Cannot be combined with \`value\`.
|
|
322
|
+
|
|
323
|
+
For blocks_only structured_text fields, you can pass an \`order\` array of block IDs to reorder blocks without constructing a full DAST document. The order array replaces the DAST children list. Cannot be combined with \`value\`. All block IDs in \`order\` must exist in the merged blocks map, and all merged blocks must appear in \`order\`.
|
|
324
|
+
|
|
333
325
|
Example — update one block's description, delete another, keep the rest:
|
|
334
|
-
{ blocks: { "block-
|
|
326
|
+
{ blocks: { "block-2": { "description": "New text" }, "block-3": null } }
|
|
327
|
+
|
|
328
|
+
Example — append a new block while patching an existing one:
|
|
329
|
+
{ blocks: { "block-2": { "description": "Updated" } }, append: [{ "_type": "venue", "name": "New Place" }] }
|
|
330
|
+
|
|
331
|
+
Example — reorder blocks on a blocks_only field:
|
|
332
|
+
{ order: ["block-3", "block-1", "block-2"], blocks: {} }`, PatchBlocksInput.fields);
|
|
335
333
|
const DeleteRecordTool = cmsTool("delete_record", "Delete a record", DeleteRecordInput.fields);
|
|
336
|
-
const GetRecordTool = cmsTool("get_record", "Get a single record by modelApiKey + recordId. Useful after search_content when you need the full materialized record, including structured_text fields, before patch_blocks or update_record.", GetRecordInput.fields);
|
|
337
|
-
const QueryRecordsTool = cmsTool("query_records", "List records for a model. Structured_text fields are materialized for inspection, including nested blocks inside parent block fields. Useful for finding record IDs before update_record, patch_blocks,
|
|
334
|
+
const GetRecordTool = cmsTool("get_record", "Get a single record by modelApiKey + recordId. Useful after search_content when you need the full materialized record, including structured_text fields, before patch_blocks or update_record. This is a workspace/content-management read tool, not a substitute for final live verification via GraphQL or the site URL.", GetRecordInput.fields);
|
|
335
|
+
const QueryRecordsTool = cmsTool("query_records", "List records for a model. Structured_text fields are materialized for inspection, including nested blocks inside parent block fields. Useful for finding record IDs before update_record, patch_blocks, set_publish_status, or record_versions. Use GraphQL or the site URL for final public/live verification after publishing.", QueryRecordsInput.fields);
|
|
338
336
|
const BulkCreateRecordsTool = cmsTool("bulk_create_records", `Create multiple records in one operation (up to 1000). Much faster than calling create_record in a loop.
|
|
339
337
|
|
|
340
338
|
All records must belong to the same model. Slugs are auto-generated. Returns {created, records}, where records is an array of objects like {id}.`, BulkCreateRecordsInput.fields);
|
|
341
|
-
const
|
|
342
|
-
const UnpublishRecordsTool = cmsTool("unpublish_records", "Unpublish one or more records.", PublishRecordsInput.fields);
|
|
339
|
+
const SetPublishStatusTool = cmsTool("set_publish_status", "Publish or unpublish one or more records. Pass recordIds as an array, even for a single record. Required-field validation is enforced at publish time for draft-enabled models.", SetPublishStatusInput.fields);
|
|
343
340
|
const ScheduleTool = cmsTool("schedule", `Schedule a record to publish or unpublish at a future ISO datetime, or clear both schedules.
|
|
344
341
|
|
|
345
342
|
action: "publish" | "unpublish" | "clear"
|
|
@@ -354,37 +351,6 @@ action: "list" | "get" | "restore"
|
|
|
354
351
|
const ReorderRecordsTool = cmsTool("reorder_records", "Reorder records in a sortable/tree model by providing ordered record IDs", ReorderInput.fields);
|
|
355
352
|
const RemoveBlockTool = cmsTool("remove_block", "Remove a block type entirely (cleans DAST trees, deletes blocks, drops table), or remove it from a specific field's whitelist (provide fieldId).", RemoveBlockInput.fields);
|
|
356
353
|
const RemoveLocaleTool = cmsTool("remove_locale", "Remove a locale and strip it from all localized field values", LocaleIdInput.fields);
|
|
357
|
-
const BuildStructuredTextTool = cmsTool("build_structured_text", `Build a StructuredText value from typed nodes or markdown, plus optional block definitions.
|
|
358
|
-
|
|
359
|
-
Two modes:
|
|
360
|
-
1. Typed nodes (provide "nodes"): precise control over document structure
|
|
361
|
-
2. Markdown (provide "markdown"): natural formatting for prose-heavy content
|
|
362
|
-
|
|
363
|
-
Workflow: prepare blocks first, then reference them in nodes or markdown.
|
|
364
|
-
|
|
365
|
-
blocks: [{id: "v1", type: "venue", data: {name: "Chickpea", image: "asset_id"}}]
|
|
366
|
-
|
|
367
|
-
Typed nodes (text structure referencing blocks by ID):
|
|
368
|
-
- paragraph: {type:"paragraph", text:"Inline **markdown** and [links](url) supported"}
|
|
369
|
-
- heading: {type:"heading", level:2, text:"Section Title"}
|
|
370
|
-
- code: {type:"code", code:"const x = 1", language:"typescript"}
|
|
371
|
-
- blockquote: {type:"blockquote", text:"Quote text"}
|
|
372
|
-
- list: {type:"list", style:"bulleted"|"numbered", items:["First","Second"]}
|
|
373
|
-
- thematicBreak: {type:"thematicBreak"}
|
|
374
|
-
- block: {type:"block", ref:"v1"} — places a block defined in the blocks array
|
|
375
|
-
|
|
376
|
-
Markdown mode: write standard markdown. Place blocks with sentinels: <!-- cms:block:BLOCK_ID -->
|
|
377
|
-
|
|
378
|
-
Inline markdown in text fields: **bold**, *italic*, \`code\`, [links](url), ~~strikethrough~~.
|
|
379
|
-
|
|
380
|
-
Encoding note: MCP tool arguments are XML-encoded by some clients. If any literal string in your structured text includes angle brackets (for example TypeScript generics like <T>, JSX, or HTML snippets), escape them as Unicode in the JSON string: \u003C and \u003E.
|
|
381
|
-
|
|
382
|
-
For nested blocks (e.g. sections containing venues), compose bottom-up:
|
|
383
|
-
1. Build inner structured text (venues) → get {value, blocks} result
|
|
384
|
-
2. Use that result as a field value in a parent block's data
|
|
385
|
-
3. Build outer structured text (sections) referencing the parent blocks
|
|
386
|
-
|
|
387
|
-
IMPORTANT: Call schema_info first to verify which block types are allowed on the target structured_text field (check the structured_text_blocks validator).`, BuildStructuredTextInput.fields);
|
|
388
354
|
const UploadAssetTool = cmsTool("upload_asset", `Register an asset after uploading the original file to R2 out of band.
|
|
389
355
|
|
|
390
356
|
Upload flow:
|
|
@@ -427,6 +393,15 @@ const UpdateSiteSettingsTool = cmsTool("update_site_settings", `Update global si
|
|
|
427
393
|
Use this tool for fields like siteName, titleSuffix, fallbackSeoTitle, and fallbackSeoDescription.
|
|
428
394
|
If your schema also has a singleton content model named site_settings with fields like tagline or logo, update that record with query_records + update_record instead of this tool.
|
|
429
395
|
When the task is specifically about the singleton record, avoid mixing both surfaces unless the user explicitly asks for both.`, UpdateSiteSettingsInput.fields);
|
|
396
|
+
const EditorTokensTool = cmsTool("editor_tokens", `Manage editor tokens: create, list, or revoke.
|
|
397
|
+
|
|
398
|
+
action: "create" | "list" | "revoke"
|
|
399
|
+
- create: provide name, optional expiresIn (seconds). Returns token for restricted write access (no schema mutations).
|
|
400
|
+
- list: returns all non-expired editor tokens.
|
|
401
|
+
- revoke: provide tokenId to revoke.`, EditorTokensInput.fields);
|
|
402
|
+
const GetPreviewUrlTool = cmsTool("get_preview_url", `Generate a preview URL for a draft record. The model must have a canonicalPathTemplate set (e.g. /posts/{slug}).
|
|
403
|
+
|
|
404
|
+
Returns a fully assembled URL with a short-lived preview token when siteUrl is configured, or the previewPath and token separately otherwise.`, GetPreviewUrlInput.fields);
|
|
430
405
|
const AdminTools = [
|
|
431
406
|
SchemaInfoTool,
|
|
432
407
|
CreateModelTool,
|
|
@@ -442,14 +417,12 @@ const AdminTools = [
|
|
|
442
417
|
GetRecordTool,
|
|
443
418
|
QueryRecordsTool,
|
|
444
419
|
BulkCreateRecordsTool,
|
|
445
|
-
|
|
446
|
-
UnpublishRecordsTool,
|
|
420
|
+
SetPublishStatusTool,
|
|
447
421
|
ScheduleTool,
|
|
448
422
|
RecordVersionsTool,
|
|
449
423
|
ReorderRecordsTool,
|
|
450
424
|
RemoveBlockTool,
|
|
451
425
|
RemoveLocaleTool,
|
|
452
|
-
BuildStructuredTextTool,
|
|
453
426
|
UploadAssetTool,
|
|
454
427
|
ImportAssetFromUrlTool,
|
|
455
428
|
ListAssetsTool,
|
|
@@ -459,12 +432,8 @@ const AdminTools = [
|
|
|
459
432
|
ReindexSearchTool,
|
|
460
433
|
GetSiteSettingsTool,
|
|
461
434
|
UpdateSiteSettingsTool,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
action: "create" | "list" | "revoke"
|
|
465
|
-
- create: provide name, optional expiresIn (seconds). Returns token for restricted write access (no schema mutations).
|
|
466
|
-
- list: returns all non-expired editor tokens.
|
|
467
|
-
- revoke: provide tokenId to revoke.`, EditorTokensInput.fields)
|
|
435
|
+
EditorTokensTool,
|
|
436
|
+
GetPreviewUrlTool
|
|
468
437
|
];
|
|
469
438
|
const EditorTools = [
|
|
470
439
|
SchemaInfoTool,
|
|
@@ -475,23 +444,30 @@ const EditorTools = [
|
|
|
475
444
|
GetRecordTool,
|
|
476
445
|
QueryRecordsTool,
|
|
477
446
|
BulkCreateRecordsTool,
|
|
478
|
-
|
|
479
|
-
UnpublishRecordsTool,
|
|
447
|
+
SetPublishStatusTool,
|
|
480
448
|
ScheduleTool,
|
|
481
449
|
RecordVersionsTool,
|
|
482
450
|
ReorderRecordsTool,
|
|
483
|
-
BuildStructuredTextTool,
|
|
484
451
|
UploadAssetTool,
|
|
485
452
|
ImportAssetFromUrlTool,
|
|
486
453
|
ListAssetsTool,
|
|
487
454
|
ReplaceAssetTool,
|
|
488
|
-
SchemaIOTool,
|
|
489
455
|
SearchContentTool,
|
|
490
456
|
GetSiteSettingsTool,
|
|
491
|
-
UpdateSiteSettingsTool
|
|
457
|
+
UpdateSiteSettingsTool,
|
|
458
|
+
GetPreviewUrlTool
|
|
492
459
|
];
|
|
493
460
|
const CmsToolkit = Toolkit.make(...AdminTools);
|
|
494
461
|
const EditorToolkit = Toolkit.make(...EditorTools);
|
|
462
|
+
/** Tool metadata for Code Mode — extracted without MCP protocol overhead */
|
|
463
|
+
function getToolMeta(mode = "admin") {
|
|
464
|
+
const toolkit = mode === "editor" ? EditorToolkit : CmsToolkit;
|
|
465
|
+
return Object.values(toolkit.tools).map((tool) => ({
|
|
466
|
+
name: tool.name,
|
|
467
|
+
description: tool.description ?? "",
|
|
468
|
+
inputSchema: toMcpInputSchema(tool)
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
495
471
|
function createGuideResource() {
|
|
496
472
|
return McpServer.resource({
|
|
497
473
|
uri: "agent-cms://guide",
|
|
@@ -505,7 +481,7 @@ Server boundary:
|
|
|
505
481
|
- Editor MCP: /mcp/editor — content/publishing/assets/search only. If a schema-mutation tool is missing, you are probably on the editor MCP and should switch surfaces instead of retrying.
|
|
506
482
|
|
|
507
483
|
Workflow order:
|
|
508
|
-
schema_info -> create_model -> create_field -> create_record ->
|
|
484
|
+
schema_info -> create_model -> create_field -> create_record -> set_publish_status
|
|
509
485
|
|
|
510
486
|
Naming conventions:
|
|
511
487
|
- api_key: snake_case (e.g. blog_post, cover_image)
|
|
@@ -521,22 +497,30 @@ Field value formats (composite types):
|
|
|
521
497
|
- link: record ID string
|
|
522
498
|
- links: array of record ID strings
|
|
523
499
|
- seo: {"title":"...","description":"...","image":"<asset_id>","twitterCard":"summary_large_image"}
|
|
524
|
-
- structured_text: {"value":{"schema":"dast","document":{...}},"blocks":{
|
|
500
|
+
- structured_text: markdown string, typed nodes array, {markdown:"...",blocks:[...]}, {nodes:[...],blocks:[...]}, or full DAST envelope {"value":{"schema":"dast","document":{...}},"blocks":{...}}
|
|
501
|
+
Prefer markdown mode for prose-heavy content. Inline formatting, links, and block placement all work. Initial block payloads are persisted by the same create_record/update_record call; patch_blocks is for later targeted edits, not for finishing initial block creation:
|
|
502
|
+
- Standard markdown: **bold**, *italic*, \`code\`, ~~strike~~, [links](url)
|
|
503
|
+
- Block refs: <!-- cms:block:BLOCK_ID --> (own line)
|
|
504
|
+
- Record links: [link text](itemLink:RECORD_ID)
|
|
505
|
+
- Inline items: <!-- cms:inlineItem:RECORD_ID -->
|
|
506
|
+
- Inline blocks: <!-- cms:inlineBlock:BLOCK_ID -->
|
|
525
507
|
- color: {"red":255,"green":0,"blue":0,"alpha":255}
|
|
526
508
|
- lat_lon: {"latitude":64.13,"longitude":-21.89}
|
|
527
509
|
|
|
528
510
|
Structured text editing notes:
|
|
529
511
|
- patch_blocks can target both top-level blocks and nested blocks inside structured_text sub-fields.
|
|
530
512
|
- If the same nested block ID exists in multiple locations, patch_blocks will ask you to patch the parent block explicitly.
|
|
513
|
+
- patch_blocks supports an \`append\` array to insert new blocks without rebuilding the DAST. Each entry needs \`_type\` plus field values. New block nodes are appended to the end of the document. The response includes \`_appendedIds\` with the generated IDs.
|
|
531
514
|
- get_record is the fastest way to inspect one known record's full materialized structured_text after search_content returns its id.
|
|
532
515
|
- query_records materializes structured_text fields for inspection; on published records, _published_snapshot remains useful as a raw snapshot of what is live.
|
|
533
516
|
|
|
534
517
|
Draft/publish lifecycle:
|
|
535
|
-
Records on draft-enabled models start as drafts. create_record returns the created draft record object, including its top-level id. Call
|
|
536
|
-
Use
|
|
518
|
+
Records on draft-enabled models start as drafts. create_record returns the created draft record object, including its top-level id. Call set_publish_status with action "publish" and that recordId to make it visible in GraphQL.
|
|
519
|
+
Use set_publish_status for both publish and unpublish, single and bulk operations — just pass an array of recordIds and the desired action.
|
|
537
520
|
Required-field validation for draft-enabled models happens at publish time, not create_record time.
|
|
538
521
|
Edits after publishing create a new draft version — publish again to update.
|
|
539
522
|
GraphQL serves published content by default; use X-Include-Drafts header for previews.
|
|
523
|
+
If a task says to verify what is live or publicly visible, do that via GraphQL or the site URL after publishing — not via query_records/get_record, which show workspace state rather than the public delivery surface.
|
|
540
524
|
|
|
541
525
|
Singletons and site settings:
|
|
542
526
|
- If a singleton exists as a normal content model in your schema (for example a site_settings record with fields like tagline), treat it like content. Use update_record without recordId for direct singleton edits, or query_records + update_record if you need to inspect first.
|
|
@@ -556,7 +540,7 @@ Asset upload flow:
|
|
|
556
540
|
Tool argument encoding:
|
|
557
541
|
- Some MCP clients XML-encode tool arguments before they reach the server.
|
|
558
542
|
- If a literal string value contains angle brackets (for example TypeScript generics like <T>, JSX, or inline HTML), escape them inside the JSON string as \u003C and \u003E.
|
|
559
|
-
- This matters most for
|
|
543
|
+
- This matters most for create_record/update_record payloads carrying code snippets in structured_text fields.
|
|
560
544
|
|
|
561
545
|
Raw HTTP / JSON-RPC access:
|
|
562
546
|
- Endpoint: POST <mount>/mcp for admin, POST <mount>/mcp/editor for editor
|
|
@@ -570,13 +554,25 @@ Raw HTTP / JSON-RPC access:
|
|
|
570
554
|
.result.content[0].text | fromjson
|
|
571
555
|
- For single-record tools like import_asset_from_url, create_record, and get_record, that parsed payload is the object itself, so use payload.id directly rather than looking for nested arrays
|
|
572
556
|
|
|
557
|
+
Draft preview:
|
|
558
|
+
Models can have a canonicalPathTemplate (e.g. /posts/{slug}) for preview URLs.
|
|
559
|
+
Tool responses include _previewPath when a template is set.
|
|
560
|
+
Use get_preview_url to generate a fully assembled preview link with a short-lived token.
|
|
561
|
+
|
|
573
562
|
Slug fields:
|
|
574
563
|
Set validator {"slug_source": "title"} to auto-generate from a source field.
|
|
575
564
|
Create the slug field AFTER the source field.
|
|
576
565
|
|
|
577
566
|
Pluralization:
|
|
578
567
|
category -> allCategories, blog_post -> allBlogPosts, person -> allPeople
|
|
579
|
-
Powered by standard English pluralization rules
|
|
568
|
+
Powered by standard English pluralization rules.
|
|
569
|
+
|
|
570
|
+
Translation:
|
|
571
|
+
For translating structured_text fields, work with the markdown representation.
|
|
572
|
+
Translate the full markdown document — not field by field — to preserve context, tone, and flow.
|
|
573
|
+
Leave block sentinels (<!-- cms:block:ID -->), record links ([text](itemLink:ID)), and inline refs untouched.
|
|
574
|
+
The CMS reconstructs DAST from the translated markdown when you update the record.
|
|
575
|
+
This gives full article context for fluid translations vs. isolated sentence-level machine translation.`)
|
|
580
576
|
});
|
|
581
577
|
}
|
|
582
578
|
function createSchemaResource() {
|
|
@@ -638,7 +634,7 @@ Follow these steps:
|
|
|
638
634
|
3. Present your plan before executing — list models, fields, and relationships.
|
|
639
635
|
4. Create models first, then fields in order (slug fields after their source).
|
|
640
636
|
5. Create a few sample records to verify the schema works.
|
|
641
|
-
6. Publish the sample records with
|
|
637
|
+
6. Publish the sample records with set_publish_status.
|
|
642
638
|
7. Show the GraphQL query that a frontend would use to fetch this content.
|
|
643
639
|
Remember: api_key snake_case -> GraphQL camelCase fields, PascalCase types.`)
|
|
644
640
|
});
|
|
@@ -695,6 +691,34 @@ function createMcpLayer(sqlLayer, options) {
|
|
|
695
691
|
url
|
|
696
692
|
} : asset;
|
|
697
693
|
}
|
|
694
|
+
/** Look up canonical_path_template for a model and resolve _previewPath if set */
|
|
695
|
+
function addPreviewPath(modelApiKey, record) {
|
|
696
|
+
return Effect.gen(function* () {
|
|
697
|
+
if (typeof record !== "object" || record === null) return record;
|
|
698
|
+
const template = (yield* (yield* SqlClient.SqlClient).unsafe("SELECT canonical_path_template FROM models WHERE api_key = ?", [modelApiKey]))[0]?.canonical_path_template;
|
|
699
|
+
if (!template) return record;
|
|
700
|
+
const previewPath = resolvePreviewPath(template, record);
|
|
701
|
+
return {
|
|
702
|
+
...record,
|
|
703
|
+
_previewPath: previewPath
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function addPreviewPathToList(modelApiKey, records) {
|
|
708
|
+
return Effect.gen(function* () {
|
|
709
|
+
if (!Array.isArray(records)) return records;
|
|
710
|
+
const template = (yield* (yield* SqlClient.SqlClient).unsafe("SELECT canonical_path_template FROM models WHERE api_key = ?", [modelApiKey]))[0]?.canonical_path_template;
|
|
711
|
+
if (!template) return records;
|
|
712
|
+
return records.map((r) => {
|
|
713
|
+
if (typeof r !== "object" || r === null) return r;
|
|
714
|
+
const previewPath = resolvePreviewPath(template, r);
|
|
715
|
+
return {
|
|
716
|
+
...r,
|
|
717
|
+
_previewPath: previewPath
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
}
|
|
698
722
|
const toolHandlers = {
|
|
699
723
|
schema_info: withDecoded(SchemaInfoInput, ({ filterByName, filterByType, includeFieldDetails }) => Effect.gen(function* () {
|
|
700
724
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -735,6 +759,7 @@ function createMcpLayer(sqlLayer, options) {
|
|
|
735
759
|
sortable: !!m.sortable,
|
|
736
760
|
tree: !!m.tree,
|
|
737
761
|
allLocalesRequired: !!m.all_locales_required,
|
|
762
|
+
canonicalPathTemplate: m.canonical_path_template ?? null,
|
|
738
763
|
...includeFieldDetails ? { fields: mFields.map((f) => ({
|
|
739
764
|
id: f.id,
|
|
740
765
|
label: f.label,
|
|
@@ -760,24 +785,27 @@ function createMcpLayer(sqlLayer, options) {
|
|
|
760
785
|
update_field: withDecoded(UpdateFieldInput, ({ fieldId, ...rest }) => updateField(fieldId, rest)),
|
|
761
786
|
delete_model: withDecoded(ModelIdInput, ({ modelId }) => deleteModel(modelId)),
|
|
762
787
|
delete_field: withDecoded(FieldIdInput, ({ fieldId }) => deleteField(fieldId)),
|
|
763
|
-
create_record: withDecoded(CreateRecordInput, (input) => createRecord(input, options?.actor)),
|
|
788
|
+
create_record: withDecoded(CreateRecordInput, (input) => createRecord(input, options?.actor).pipe(Effect.flatMap((r) => addPreviewPath(input.modelApiKey, r)))),
|
|
764
789
|
update_record: withDecoded(UpdateRecordInput, ({ recordId, modelApiKey, data }) => {
|
|
765
|
-
|
|
790
|
+
return (recordId ? patchRecord(recordId, {
|
|
766
791
|
modelApiKey,
|
|
767
792
|
data
|
|
768
|
-
}, options?.actor);
|
|
769
|
-
return updateSingletonRecord(modelApiKey, data, options?.actor);
|
|
793
|
+
}, options?.actor) : updateSingletonRecord(modelApiKey, data, options?.actor)).pipe(Effect.flatMap((r) => addPreviewPath(modelApiKey, r)));
|
|
770
794
|
}),
|
|
771
|
-
patch_blocks: withDecoded(PatchBlocksInput, (input) => patchBlocksForField(input, options?.actor))
|
|
795
|
+
patch_blocks: withDecoded(PatchBlocksInput, (input) => patchBlocksForField(input, options?.actor).pipe(Effect.map((record) => {
|
|
796
|
+
const deletedIds = Object.entries(input.blocks).filter(([, v]) => v === null).map(([k]) => k);
|
|
797
|
+
return compactPatchBlocksResponse(record, input.fieldApiKey, deletedIds);
|
|
798
|
+
}))),
|
|
772
799
|
delete_record: withDecoded(DeleteRecordInput, ({ recordId, modelApiKey }) => removeRecord(modelApiKey, recordId)),
|
|
773
|
-
get_record: withDecoded(GetRecordInput, ({ recordId, modelApiKey }) => getRecord(modelApiKey, recordId)),
|
|
774
|
-
query_records: withDecoded(QueryRecordsInput, ({ modelApiKey }) => listRecords(modelApiKey)),
|
|
800
|
+
get_record: withDecoded(GetRecordInput, ({ recordId, modelApiKey }) => getRecord(modelApiKey, recordId).pipe(Effect.flatMap((r) => addPreviewPath(modelApiKey, r)))),
|
|
801
|
+
query_records: withDecoded(QueryRecordsInput, ({ modelApiKey }) => listRecords(modelApiKey).pipe(Effect.flatMap((r) => addPreviewPathToList(modelApiKey, r)))),
|
|
775
802
|
bulk_create_records: withDecoded(BulkCreateRecordsInput, ({ modelApiKey, records }) => bulkCreateRecords({
|
|
776
803
|
modelApiKey,
|
|
777
804
|
records
|
|
778
805
|
}, options?.actor)),
|
|
779
|
-
|
|
780
|
-
|
|
806
|
+
set_publish_status: withDecoded(SetPublishStatusInput, ({ action, recordIds, modelApiKey }) => {
|
|
807
|
+
return action === "publish" ? recordIds.length === 1 ? publishRecord(modelApiKey, recordIds[0], options?.actor) : bulkPublishRecords(modelApiKey, recordIds, options?.actor) : recordIds.length === 1 ? unpublishRecord(modelApiKey, recordIds[0], options?.actor) : bulkUnpublishRecords(modelApiKey, recordIds, options?.actor);
|
|
808
|
+
}),
|
|
781
809
|
schedule: withDecoded(ScheduleInput, ({ recordId, modelApiKey, action, at }) => {
|
|
782
810
|
if (action === "clear") return clearSchedule(modelApiKey, recordId, options?.actor);
|
|
783
811
|
if (action === "publish") return schedulePublish(modelApiKey, recordId, at ?? null, options?.actor);
|
|
@@ -807,88 +835,6 @@ function createMcpLayer(sqlLayer, options) {
|
|
|
807
835
|
return removeBlockType(blockApiKey);
|
|
808
836
|
}),
|
|
809
837
|
remove_locale: withDecoded(LocaleIdInput, ({ localeId }) => removeLocale(localeId)),
|
|
810
|
-
build_structured_text: withDecoded(BuildStructuredTextInput, ({ markdown, blocks, nodes }) => Effect.sync(() => {
|
|
811
|
-
const blockMap = {};
|
|
812
|
-
for (const b of blocks ?? []) blockMap[b.id] = {
|
|
813
|
-
_type: b.type,
|
|
814
|
-
...b.data
|
|
815
|
-
};
|
|
816
|
-
if (markdown != null) {
|
|
817
|
-
const doc = markdownToDast(markdown);
|
|
818
|
-
const refs = collectBlockRefs(doc.document);
|
|
819
|
-
assertKnownBlockRefs(blockMap, refs);
|
|
820
|
-
assertNoUnusedBlocks(blockMap, refs);
|
|
821
|
-
return {
|
|
822
|
-
value: doc,
|
|
823
|
-
blocks: blockMap
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
const children = [];
|
|
827
|
-
for (const node of nodes ?? []) switch (node.type) {
|
|
828
|
-
case "paragraph":
|
|
829
|
-
children.push({
|
|
830
|
-
type: "paragraph",
|
|
831
|
-
children: parseInlineSpans(node.text)
|
|
832
|
-
});
|
|
833
|
-
break;
|
|
834
|
-
case "heading":
|
|
835
|
-
children.push({
|
|
836
|
-
type: "heading",
|
|
837
|
-
level: node.level,
|
|
838
|
-
children: parseInlineSpans(node.text)
|
|
839
|
-
});
|
|
840
|
-
break;
|
|
841
|
-
case "code":
|
|
842
|
-
children.push({
|
|
843
|
-
type: "code",
|
|
844
|
-
code: node.code,
|
|
845
|
-
...node.language ? { language: node.language } : {}
|
|
846
|
-
});
|
|
847
|
-
break;
|
|
848
|
-
case "blockquote":
|
|
849
|
-
children.push({
|
|
850
|
-
type: "blockquote",
|
|
851
|
-
children: [{
|
|
852
|
-
type: "paragraph",
|
|
853
|
-
children: parseInlineSpans(node.text)
|
|
854
|
-
}]
|
|
855
|
-
});
|
|
856
|
-
break;
|
|
857
|
-
case "list":
|
|
858
|
-
children.push({
|
|
859
|
-
type: "list",
|
|
860
|
-
style: node.style ?? "bulleted",
|
|
861
|
-
children: node.items.map((item) => ({
|
|
862
|
-
type: "listItem",
|
|
863
|
-
children: [{
|
|
864
|
-
type: "paragraph",
|
|
865
|
-
children: parseInlineSpans(item)
|
|
866
|
-
}]
|
|
867
|
-
}))
|
|
868
|
-
});
|
|
869
|
-
break;
|
|
870
|
-
case "thematicBreak":
|
|
871
|
-
children.push({ type: "thematicBreak" });
|
|
872
|
-
break;
|
|
873
|
-
case "block":
|
|
874
|
-
children.push({
|
|
875
|
-
type: "block",
|
|
876
|
-
item: node.ref
|
|
877
|
-
});
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
assertKnownBlockRefs(blockMap, collectBlockRefs(children));
|
|
881
|
-
return {
|
|
882
|
-
value: {
|
|
883
|
-
schema: "dast",
|
|
884
|
-
document: {
|
|
885
|
-
type: "root",
|
|
886
|
-
children
|
|
887
|
-
}
|
|
888
|
-
},
|
|
889
|
-
blocks: blockMap
|
|
890
|
-
};
|
|
891
|
-
})),
|
|
892
838
|
upload_asset: withDecoded(CreateAssetInput, (input) => createAsset(input, options?.actor).pipe(Effect.map(withAssetUrl))),
|
|
893
839
|
import_asset_from_url: withDecoded(ImportAssetFromUrlInput, (input) => importAssetFromUrl(input, options?.actor).pipe(Effect.map(withAssetUrl))),
|
|
894
840
|
list_assets: () => listAssets().pipe(Effect.map((assets) => assets.map((a) => {
|
|
@@ -911,6 +857,29 @@ function createMcpLayer(sqlLayer, options) {
|
|
|
911
857
|
reindex_search: withDecoded(ReindexSearchInput, ({ modelApiKey }) => reindexAll(modelApiKey)),
|
|
912
858
|
get_site_settings: () => getSiteSettings(),
|
|
913
859
|
update_site_settings: withDecoded(UpdateSiteSettingsInput, updateSiteSettings),
|
|
860
|
+
get_preview_url: withDecoded(GetPreviewUrlInput, ({ recordId, modelApiKey }) => Effect.gen(function* () {
|
|
861
|
+
const model = yield* getModelByApiKey(modelApiKey);
|
|
862
|
+
if (!model.canonical_path_template) return yield* Effect.fail({
|
|
863
|
+
_tag: "ValidationError",
|
|
864
|
+
message: `Model '${modelApiKey}' has no canonicalPathTemplate configured`
|
|
865
|
+
});
|
|
866
|
+
const record = yield* getRecord(modelApiKey, recordId);
|
|
867
|
+
const recordData = typeof record === "object" && record !== null ? record : {};
|
|
868
|
+
const previewPath = resolvePreviewPath(model.canonical_path_template, recordData);
|
|
869
|
+
const { token, expiresAt } = yield* createPreviewToken();
|
|
870
|
+
const siteUrl = options?.siteUrl;
|
|
871
|
+
if (siteUrl) return {
|
|
872
|
+
url: `${siteUrl.replace(/\/$/, "")}/api/draft-mode/enable?token=${encodeURIComponent(token)}&redirect=${encodeURIComponent(previewPath)}`,
|
|
873
|
+
previewPath,
|
|
874
|
+
token,
|
|
875
|
+
expiresAt
|
|
876
|
+
};
|
|
877
|
+
return {
|
|
878
|
+
previewPath,
|
|
879
|
+
token,
|
|
880
|
+
expiresAt
|
|
881
|
+
};
|
|
882
|
+
})),
|
|
914
883
|
editor_tokens: withDecoded(EditorTokensInput, ({ action, name, expiresIn, tokenId }) => {
|
|
915
884
|
if (action === "list") return listEditorTokens();
|
|
916
885
|
if (action === "create") {
|
|
@@ -978,16 +947,6 @@ function createMcpLayer(sqlLayer, options) {
|
|
|
978
947
|
return Layer.merge(serverLayer, registeredContent).pipe(Layer.provide(fullLayer));
|
|
979
948
|
}
|
|
980
949
|
//#endregion
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* Create a cached Web Standard handler for the Effect-native MCP server.
|
|
984
|
-
*/
|
|
985
|
-
function createMcpHttpHandler(sqlLayer, options) {
|
|
986
|
-
const mcpLayer = createMcpLayer(sqlLayer, options);
|
|
987
|
-
const { handler } = HttpLayerRouter.toWebHandler(mcpLayer, { disableLogger: true });
|
|
988
|
-
return handler;
|
|
989
|
-
}
|
|
990
|
-
//#endregion
|
|
991
|
-
export { createMcpHttpHandler };
|
|
950
|
+
export { getToolMeta as n, createMcpLayer as t };
|
|
992
951
|
|
|
993
|
-
//# sourceMappingURL=
|
|
952
|
+
//# sourceMappingURL=server-D0XqvDjU.mjs.map
|