agent-cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,993 @@
1
+ import { $ as listRecords, A as scheduleUnpublish, C as SearchInput, D as clearSchedule, F as removeBlockType, H as listAssets, I as removeLocale, J as publishRecord, K as bulkPublishRecords, L as AssetImportContext, P as removeBlockFromWhitelist, Q as getRecord, R as createAsset, U as replaceAsset, V as importAssetFromUrl, X as bulkCreateRecords, Y as unpublishRecord, Z as createRecord, _t as updateModel, a as exportSchema, at as getVersion, b as ReorderInput, bt as VectorizeContext, c as CreateAssetInput, ct as HooksContext, et as patchBlocksForField, f as CreateModelInput, ft as updateField, g as ImportSchemaInput, h as ImportAssetFromUrlInput, it as updateSingletonRecord, k as schedulePublish, lt as createField, mt as deleteModel, n as listEditorTokens, nt as removeRecord, o as importSchema, ot as listVersions, p as CreateRecordInput, pt as createModel, q as bulkUnpublishRecords, r as revokeEditorToken, rt as reorderRecords, st as restoreVersion, t as createEditorToken, tt as patchRecord, u as CreateFieldInput, ut as deleteField, vt as reindexAll, y as ReindexSearchInput, yt as search } from "./token-service-BDjccMmz.mjs";
2
+ import { B as encodeJson, J as ValidationError, K as SchemaEngineError, L as decodeJsonRecordStringOr, m as markdownToDast } from "./structured-text-service-B4xSlUg_.mjs";
3
+ import { Context, Effect, Layer, Option, Schema } from "effect";
4
+ import { SqlClient } from "@effect/sql";
5
+ import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter";
6
+ import * as McpServer from "@effect/ai/McpServer";
7
+ import * as McpSchema from "@effect/ai/McpSchema";
8
+ import * as AiTool from "@effect/ai/Tool";
9
+ import * as Toolkit from "@effect/ai/Toolkit";
10
+ //#region src/services/site-settings-service.ts
11
+ const fieldMap = {
12
+ siteName: "site_name",
13
+ titleSuffix: "title_suffix",
14
+ noIndex: "no_index",
15
+ faviconId: "favicon_id",
16
+ facebookPageUrl: "facebook_page_url",
17
+ twitterAccount: "twitter_account",
18
+ fallbackSeoTitle: "fallback_seo_title",
19
+ fallbackSeoDescription: "fallback_seo_description",
20
+ fallbackSeoImageId: "fallback_seo_image_id",
21
+ fallbackSeoTwitterCard: "fallback_seo_twitter_card"
22
+ };
23
+ function mapMissingTable(error) {
24
+ if ((error instanceof Error ? error.message : String(error)).includes("no such table: site_settings")) return new SchemaEngineError({
25
+ message: "site_settings table not found. Run setup/migrations before using site settings.",
26
+ cause: error
27
+ });
28
+ return new SchemaEngineError({
29
+ message: "Failed to access site settings",
30
+ cause: error
31
+ });
32
+ }
33
+ function getSiteSettings() {
34
+ return Effect.gen(function* () {
35
+ const rows = yield* (yield* SqlClient.SqlClient).unsafe("SELECT * FROM site_settings LIMIT 1");
36
+ return rows.length > 0 ? rows[0] : { message: "No site settings configured yet. Use update_site_settings to create them." };
37
+ }).pipe(Effect.mapError(mapMissingTable));
38
+ }
39
+ function updateSiteSettings(args) {
40
+ return Effect.gen(function* () {
41
+ const sql = yield* SqlClient.SqlClient;
42
+ const sets = [];
43
+ const params = [];
44
+ for (const [key, value] of Object.entries(args)) {
45
+ const col = fieldMap[key];
46
+ sets.push(`"${col}" = ?`);
47
+ params.push(typeof value === "boolean" ? value ? 1 : 0 : value);
48
+ }
49
+ if (sets.length === 0) return yield* new ValidationError({ message: "No fields to update" });
50
+ sets.push(`"updated_at" = datetime('now')`);
51
+ yield* sql.unsafe(`INSERT OR IGNORE INTO site_settings (id) VALUES ('default')`);
52
+ yield* sql.unsafe(`UPDATE site_settings SET ${sets.join(", ")} WHERE id = 'default'`, params);
53
+ return (yield* sql.unsafe("SELECT * FROM site_settings WHERE id = 'default'"))[0];
54
+ }).pipe(Effect.mapError((error) => {
55
+ if (error instanceof ValidationError) return error;
56
+ return mapMissingTable(error);
57
+ }));
58
+ }
59
+ //#endregion
60
+ //#region src/mcp/server.ts
61
+ /**
62
+ * Effect-native MCP (Model Context Protocol) server for agent-cms.
63
+ * 3-layer architecture: Discovery -> Schema -> Content
64
+ */
65
+ const JsonRecord = Schema.Record({
66
+ key: Schema.String,
67
+ value: Schema.Unknown
68
+ });
69
+ const CommonDependencies = [
70
+ SqlClient.SqlClient,
71
+ VectorizeContext,
72
+ HooksContext,
73
+ AssetImportContext
74
+ ];
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
+ const UpdateModelInput = Schema.Struct({
107
+ modelId: Schema.String,
108
+ name: Schema.optional(Schema.String),
109
+ apiKey: Schema.optional(Schema.String),
110
+ singleton: Schema.optional(Schema.Boolean),
111
+ sortable: Schema.optional(Schema.Boolean),
112
+ hasDraft: Schema.optional(Schema.Boolean),
113
+ allLocalesRequired: Schema.optional(Schema.Boolean)
114
+ });
115
+ const UpdateFieldInput = Schema.Struct({
116
+ fieldId: Schema.String,
117
+ label: Schema.optional(Schema.String),
118
+ apiKey: Schema.optional(Schema.String),
119
+ validators: Schema.optional(JsonRecord),
120
+ hint: Schema.optional(Schema.String)
121
+ });
122
+ const ModelIdInput = Schema.Struct({ modelId: Schema.String });
123
+ const FieldIdInput = Schema.Struct({ fieldId: Schema.String });
124
+ const LocaleIdInput = Schema.Struct({ localeId: Schema.String });
125
+ const SchemaInfoInput = Schema.Struct({
126
+ filterByName: Schema.optional(Schema.String),
127
+ filterByType: Schema.optional(Schema.Literal("model", "block")),
128
+ includeFieldDetails: Schema.optionalWith(Schema.Boolean, { default: () => true })
129
+ });
130
+ const UpdateRecordInput = Schema.Struct({
131
+ recordId: Schema.optional(Schema.String),
132
+ modelApiKey: Schema.String,
133
+ data: Schema.optionalWith(JsonRecord, { default: () => ({}) })
134
+ });
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
+ const DeleteRecordInput = Schema.Struct({
146
+ recordId: Schema.String,
147
+ modelApiKey: Schema.String
148
+ });
149
+ const QueryRecordsInput = Schema.Struct({ modelApiKey: Schema.String });
150
+ const BulkCreateRecordsInput = Schema.Struct({
151
+ modelApiKey: Schema.String,
152
+ records: Schema.Array(JsonRecord)
153
+ });
154
+ const PublishRecordsInput = Schema.Struct({
155
+ modelApiKey: Schema.String,
156
+ 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
+ });
158
+ const ScheduleInput = Schema.Struct({
159
+ recordId: Schema.String,
160
+ modelApiKey: Schema.String,
161
+ action: Schema.Literal("publish", "unpublish", "clear"),
162
+ at: Schema.optional(Schema.NullOr(Schema.String))
163
+ });
164
+ const RecordVersionsInput = Schema.Struct({
165
+ action: Schema.Literal("list", "get", "restore"),
166
+ modelApiKey: Schema.String,
167
+ recordId: Schema.String,
168
+ versionId: Schema.optional(Schema.String)
169
+ });
170
+ const RemoveBlockInput = Schema.Struct({
171
+ blockApiKey: Schema.String,
172
+ fieldId: Schema.optional(Schema.String)
173
+ });
174
+ const ReplaceAssetInput = Schema.Struct({
175
+ assetId: Schema.String,
176
+ ...CreateAssetInput.fields
177
+ });
178
+ const SchemaIOInput = Schema.Struct({
179
+ action: Schema.Literal("export", "import"),
180
+ schema: Schema.optional(ImportSchemaInput)
181
+ });
182
+ const UpdateSiteSettingsInput = Schema.Struct({
183
+ siteName: Schema.optional(Schema.String),
184
+ titleSuffix: Schema.optional(Schema.String),
185
+ noIndex: Schema.optional(Schema.Boolean),
186
+ faviconId: Schema.optional(Schema.String),
187
+ facebookPageUrl: Schema.optional(Schema.String),
188
+ twitterAccount: Schema.optional(Schema.String),
189
+ fallbackSeoTitle: Schema.optional(Schema.String),
190
+ fallbackSeoDescription: Schema.optional(Schema.String),
191
+ fallbackSeoImageId: Schema.optional(Schema.String),
192
+ fallbackSeoTwitterCard: Schema.optional(Schema.String)
193
+ });
194
+ const EditorTokensInput = Schema.Struct({
195
+ action: Schema.Literal("create", "list", "revoke"),
196
+ name: Schema.optional(Schema.String),
197
+ expiresIn: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive())),
198
+ tokenId: Schema.optional(Schema.String)
199
+ });
200
+ const GetRecordInput = Schema.Struct({
201
+ recordId: Schema.String,
202
+ modelApiKey: Schema.String
203
+ });
204
+ const SetupContentModelPromptInput = Schema.Struct({ description: Schema.String });
205
+ const GenerateGraphqlQueriesPromptInput = Schema.Struct({ modelApiKey: Schema.String });
206
+ function cmsTool(name, description, parameters) {
207
+ let tool = AiTool.make(name, {
208
+ description,
209
+ parameters: parameters ?? {},
210
+ success: Schema.Unknown,
211
+ failure: Schema.Unknown,
212
+ dependencies: CommonDependencies
213
+ });
214
+ const isReadonly = name.startsWith("query_") || name.startsWith("get_") || name === "schema_info" || name === "build_structured_text" || name === "search_content";
215
+ tool = tool.annotate(AiTool.Readonly, isReadonly);
216
+ tool = tool.annotate(AiTool.Idempotent, isReadonly || name.startsWith("update_") || name.startsWith("replace_"));
217
+ tool = tool.annotate(AiTool.Destructive, name.startsWith("delete_") || name === "remove_block");
218
+ tool = tool.annotate(AiTool.OpenWorld, name === "search_content");
219
+ return tool;
220
+ }
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
+ function toStructuredContent(value) {
251
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
252
+ }
253
+ function isToolPayload(value) {
254
+ return value !== null && typeof value === "object";
255
+ }
256
+ function parseValidators(value) {
257
+ if (value == null || value === "") return {};
258
+ if (typeof value === "string") return decodeJsonRecordStringOr(value, {});
259
+ if (typeof value === "object" && !Array.isArray(value)) return value;
260
+ return {};
261
+ }
262
+ function withDecoded(schema, handler) {
263
+ return (params) => Schema.decodeUnknown(schema)(params).pipe(Effect.flatMap(handler));
264
+ }
265
+ function toMcpInputSchema(tool) {
266
+ const inputSchema = AiTool.getJsonSchema(tool);
267
+ return typeof inputSchema === "object" && "type" in inputSchema && inputSchema.type === "object" ? inputSchema : {
268
+ type: "object",
269
+ properties: {},
270
+ additionalProperties: false
271
+ };
272
+ }
273
+ function pickToolkitHandlers(toolkit, handlers) {
274
+ const filtered = {};
275
+ for (const name of Object.keys(toolkit.tools)) filtered[name] = handlers[name];
276
+ return filtered;
277
+ }
278
+ const SchemaInfoTool = cmsTool("schema_info", "Get the complete CMS schema in one call — models, block types, fields, relations. The primary tool for understanding the content model. Use filterByName to find a specific model.", SchemaInfoInput.fields);
279
+ const CreateModelTool = cmsTool("create_model", "Create a content model or block type. Use isBlock:true for block types (embeddable in StructuredText). Use singleton:true for models with exactly one record (e.g. site settings). After creating a model, add fields with create_field.", CreateModelInput.fields);
280
+ const UpdateModelTool = cmsTool("update_model", "Update model properties (name, apiKey, singleton, sortable, hasDraft, allLocalesRequired, ordering). Set ordering to a default sort like 'title_ASC', '_createdAt_DESC', '_position_ASC', or null to clear.", UpdateModelInput.fields);
281
+ const CreateFieldTool = cmsTool("create_field", `Add a field to a model. Auto-migrates the database table (adds column).
282
+
283
+ Key validators by field type:
284
+ - slug: {"slug_source": "title"} — auto-generates from source field
285
+ - string/text/slug: {"enum": ["draft","review","published"]} — restrict allowed values
286
+ - string/text/slug: {"length": {"min": 10, "max": 160}} — character count limits
287
+ - integer/float: {"number_range": {"min": 1, "max": 5}} — numeric bounds
288
+ - string/text/slug: {"format": "email"} or {"format": "url"} or {"format": {"custom_pattern": "^[A-Z]{2}\\\\d{4}$"}} — string format checks
289
+ - date/date_time: {"date_range": {"min": "now"}} — temporal bounds
290
+ - link: {"item_item_type": ["model_api_key"]} — target model
291
+ - links: {"items_item_type": ["model_api_key"]} — target model
292
+ - structured_text: {"structured_text_blocks": ["block_api_key"]} — allowed block types
293
+ - any field: {"required": true} — field is required (provide default_value for existing records)`, {
294
+ modelId: Schema.String,
295
+ ...CreateFieldInput.fields
296
+ });
297
+ const UpdateFieldTool = cmsTool("update_field", "Update field properties (label, apiKey, validators, hint)", UpdateFieldInput.fields);
298
+ const DeleteModelTool = cmsTool("delete_model", "Delete a model (fails if referenced)", ModelIdInput.fields);
299
+ 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 publish_records to make them visible in GraphQL.
301
+
302
+ Validation note:
303
+ - For models with drafts (has_draft=true), required-field validation is deferred until publish_records.
304
+ - For models without drafts (has_draft=false), required fields are enforced during create_record.
305
+
306
+ Field value formats:
307
+ - media: asset ID string, or {"upload_id":"<asset_id>","alt":"...","title":"...","focal_point":{"x":0.5,"y":0.2},"custom_data":{...}}
308
+ - media_gallery: array of asset IDs and/or media override objects
309
+ - link: record ID string
310
+ - links: array of record ID strings
311
+ - seo: {"title":"...","description":"...","image":"<asset_id>","twitterCard":"summary_large_image"}
312
+ - structured_text: {"value":{"schema":"dast","document":{...}},"blocks":{"<id>":{"_type":"block_api_key",...}}}
313
+ - color: {"red":255,"green":0,"blue":0,"alpha":255}
314
+ - lat_lon: {"latitude":64.13,"longitude":-21.89}`, CreateRecordInput.fields);
315
+ const UpdateRecordTool = cmsTool("update_record", "Update record fields. For singletons, recordId can be omitted — the single record is found automatically.", UpdateRecordInput.fields);
316
+ const PatchBlocksTool = cmsTool("patch_blocks", `Partially update blocks in a structured text field without resending the entire content tree.
317
+
318
+ You can target block IDs from either:
319
+ - the field's top-level \`blocks\` map, or
320
+ - nested structured_text sub-fields stored inside those blocks
321
+
322
+ Patch map semantics for each block ID:
323
+ - string value (the block ID) → keep block unchanged
324
+ - object with field overrides → merge into existing block (only specified fields updated)
325
+ - null → delete block and auto-prune from the relevant DAST tree
326
+
327
+ Block IDs not in the patch map are kept unchanged.
328
+
329
+ 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
+
331
+ Optionally provide a new top-level DAST \`value\`. If omitted, the existing DAST is preserved (with deleted top-level blocks auto-pruned).
332
+
333
+ Example — update one block's description, delete another, keep the rest:
334
+ { blocks: { "block-1": "block-1", "block-2": { "description": "New text" }, "block-3": null } }`, PatchBlocksInput.fields);
335
+ 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, publish_records, or record_versions.", QueryRecordsInput.fields);
338
+ 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
+
340
+ 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 PublishRecordsTool = cmsTool("publish_records", "Publish one or more records. Required/unique validation is enforced at publish time for draft-enabled models. Pass recordIds as an array, even for a single record.", PublishRecordsInput.fields);
342
+ const UnpublishRecordsTool = cmsTool("unpublish_records", "Unpublish one or more records.", PublishRecordsInput.fields);
343
+ const ScheduleTool = cmsTool("schedule", `Schedule a record to publish or unpublish at a future ISO datetime, or clear both schedules.
344
+
345
+ action: "publish" | "unpublish" | "clear"
346
+ - publish/unpublish: provide at (ISO datetime string)
347
+ - clear: at is ignored`, ScheduleInput.fields);
348
+ const RecordVersionsTool = cmsTool("record_versions", `Manage record versions: list all versions, get a snapshot, or restore a previous version.
349
+
350
+ action: "list" | "get" | "restore"
351
+ - list: returns all version snapshots for a record, newest first
352
+ - get: returns a specific version snapshot (provide versionId)
353
+ - restore: restores a previous version (provide versionId). Current state is versioned first, so restore is always reversible.`, RecordVersionsInput.fields);
354
+ const ReorderRecordsTool = cmsTool("reorder_records", "Reorder records in a sortable/tree model by providing ordered record IDs", ReorderInput.fields);
355
+ 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
+ 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
+ const UploadAssetTool = cmsTool("upload_asset", `Register an asset after uploading the original file to R2 out of band.
389
+
390
+ Upload flow:
391
+ 1. Upload the original file to R2
392
+ 2. Call this tool with the r2Key, filename, mimeType, and image dimensions
393
+ 3. The asset metadata is registered and can be referenced in media fields by its ID`, CreateAssetInput.fields);
394
+ const ImportAssetFromUrlTool = cmsTool("import_asset_from_url", `Download an asset from a public URL, store it in R2, and register it in one step.
395
+
396
+ Use this when you have an image URL and want an agent-friendly path.
397
+
398
+ Flow:
399
+ 1. Provide the source URL
400
+ 2. The CMS fetches the file (following normal public HTTP redirects), stores it in R2, and creates the asset record
401
+ 3. Use the returned asset ID in media fields (e.g. {image: "<asset_id>"})
402
+
403
+ The response includes id, r2Key, url (full public URL), and metadata. The id is what you pass to media fields — the CMS validates that the asset exists when creating/updating records.`, ImportAssetFromUrlInput.fields);
404
+ const ListAssetsTool = cmsTool("list_assets", "List all assets with their IDs, filenames, and R2 keys");
405
+ const ReplaceAssetTool = cmsTool("replace_asset", `Replace an asset's file metadata while keeping the same ID and URL. All content references remain stable.
406
+
407
+ Flow:
408
+ 1. Upload the new original file to R2
409
+ 2. Call this tool with the asset ID and new file metadata
410
+ 3. The asset URL stays the same — no broken links in content`, ReplaceAssetInput.fields);
411
+ const SchemaIOTool = cmsTool("schema_io", `Export or import the full CMS schema as portable JSON.
412
+
413
+ action: "export" | "import"
414
+ - export: returns schema JSON (models, fields, locales). No IDs — references use api_keys.
415
+ - import: creates all locales, models, and fields in dependency order from provided schema. Use on a fresh/empty CMS.
416
+ Schema format: { "version": 1, "locales": [...], "models": [{ "name", "apiKey", "fields": [...] }] }`, SchemaIOInput.fields);
417
+ const SearchContentTool = cmsTool("search_content", `Search content records. Supports keyword search (FTS5), semantic search (Vectorize), or hybrid (both combined with rank fusion).
418
+
419
+ Keyword mode: phrases ("exact match"), prefix (word*), boolean (AND/OR).
420
+ Semantic mode: finds conceptually related content even when vocabulary differs (requires AI+Vectorize bindings).
421
+ Hybrid mode (default when Vectorize available): combines both for best results.
422
+ Results include modelApiKey, recordId, title when available, rank, and snippet.`, SearchInput.fields);
423
+ const ReindexSearchTool = cmsTool("reindex_search", "Rebuild FTS5 + Vectorize search indexes. Use after deploying search to a CMS with existing content, or to recover from index drift. Scoped to a single model or all content models.", ReindexSearchInput.fields);
424
+ const GetSiteSettingsTool = cmsTool("get_site_settings", "Get global site settings from the built-in site_settings table (site name, title suffix, global SEO, favicon, social accounts). This is separate from any content-model singleton also named site_settings.");
425
+ const UpdateSiteSettingsTool = cmsTool("update_site_settings", `Update global site settings in the built-in site_settings table. These power the _site GraphQL query (globalSeo, faviconMetaTags).
426
+
427
+ Use this tool for fields like siteName, titleSuffix, fallbackSeoTitle, and fallbackSeoDescription.
428
+ 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
+ When the task is specifically about the singleton record, avoid mixing both surfaces unless the user explicitly asks for both.`, UpdateSiteSettingsInput.fields);
430
+ const AdminTools = [
431
+ SchemaInfoTool,
432
+ CreateModelTool,
433
+ UpdateModelTool,
434
+ CreateFieldTool,
435
+ UpdateFieldTool,
436
+ DeleteModelTool,
437
+ DeleteFieldTool,
438
+ CreateRecordTool,
439
+ UpdateRecordTool,
440
+ PatchBlocksTool,
441
+ DeleteRecordTool,
442
+ GetRecordTool,
443
+ QueryRecordsTool,
444
+ BulkCreateRecordsTool,
445
+ PublishRecordsTool,
446
+ UnpublishRecordsTool,
447
+ ScheduleTool,
448
+ RecordVersionsTool,
449
+ ReorderRecordsTool,
450
+ RemoveBlockTool,
451
+ RemoveLocaleTool,
452
+ BuildStructuredTextTool,
453
+ UploadAssetTool,
454
+ ImportAssetFromUrlTool,
455
+ ListAssetsTool,
456
+ ReplaceAssetTool,
457
+ SchemaIOTool,
458
+ SearchContentTool,
459
+ ReindexSearchTool,
460
+ GetSiteSettingsTool,
461
+ UpdateSiteSettingsTool,
462
+ cmsTool("editor_tokens", `Manage editor tokens: create, list, or revoke.
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)
468
+ ];
469
+ const EditorTools = [
470
+ SchemaInfoTool,
471
+ CreateRecordTool,
472
+ UpdateRecordTool,
473
+ PatchBlocksTool,
474
+ DeleteRecordTool,
475
+ GetRecordTool,
476
+ QueryRecordsTool,
477
+ BulkCreateRecordsTool,
478
+ PublishRecordsTool,
479
+ UnpublishRecordsTool,
480
+ ScheduleTool,
481
+ RecordVersionsTool,
482
+ ReorderRecordsTool,
483
+ BuildStructuredTextTool,
484
+ UploadAssetTool,
485
+ ImportAssetFromUrlTool,
486
+ ListAssetsTool,
487
+ ReplaceAssetTool,
488
+ SchemaIOTool,
489
+ SearchContentTool,
490
+ GetSiteSettingsTool,
491
+ UpdateSiteSettingsTool
492
+ ];
493
+ const CmsToolkit = Toolkit.make(...AdminTools);
494
+ const EditorToolkit = Toolkit.make(...EditorTools);
495
+ function createGuideResource() {
496
+ return McpServer.resource({
497
+ uri: "agent-cms://guide",
498
+ name: "agent-cms-guide",
499
+ description: "Orientation guide for agents: workflow, naming conventions, field formats, and lifecycle",
500
+ mimeType: "text/plain",
501
+ content: Effect.succeed(`agent-cms — Agent Orientation Guide
502
+
503
+ Server boundary:
504
+ - Admin MCP: /mcp — includes schema mutation tools like create_model, create_field, delete_model, delete_field, schema_io, and token management.
505
+ - 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
+
507
+ Workflow order:
508
+ schema_info -> create_model -> create_field -> create_record -> publish_records
509
+
510
+ Naming conventions:
511
+ - api_key: snake_case (e.g. blog_post, cover_image)
512
+ - GraphQL types: PascalCase (BlogPost, CoverImageRecord)
513
+ - GraphQL fields: camelCase (coverImage, blogPost)
514
+ - GraphQL list queries: allBlogPosts, allCategories
515
+ - GraphQL single queries: blogPost, category
516
+ - Block types get "Record" suffix in GraphQL: code_block -> CodeBlockRecord
517
+
518
+ Field value formats (composite types):
519
+ - media: asset ID string, or {"upload_id":"<asset_id>","alt":"...","title":"...","focal_point":{"x":0.5,"y":0.2},"custom_data":{...}}
520
+ - media_gallery: array of asset IDs and/or media override objects
521
+ - link: record ID string
522
+ - links: array of record ID strings
523
+ - seo: {"title":"...","description":"...","image":"<asset_id>","twitterCard":"summary_large_image"}
524
+ - structured_text: {"value":{"schema":"dast","document":{...}},"blocks":{"<id>":{"_type":"block_api_key",...}}}
525
+ - color: {"red":255,"green":0,"blue":0,"alpha":255}
526
+ - lat_lon: {"latitude":64.13,"longitude":-21.89}
527
+
528
+ Structured text editing notes:
529
+ - patch_blocks can target both top-level blocks and nested blocks inside structured_text sub-fields.
530
+ - If the same nested block ID exists in multiple locations, patch_blocks will ask you to patch the parent block explicitly.
531
+ - get_record is the fastest way to inspect one known record's full materialized structured_text after search_content returns its id.
532
+ - query_records materializes structured_text fields for inspection; on published records, _published_snapshot remains useful as a raw snapshot of what is live.
533
+
534
+ 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 publish_records with that recordId to make it visible in GraphQL.
536
+ Use publish_records and unpublish_records for both single and bulk operations — just pass an array of recordIds.
537
+ Required-field validation for draft-enabled models happens at publish time, not create_record time.
538
+ Edits after publishing create a new draft version — publish again to update.
539
+ GraphQL serves published content by default; use X-Include-Drafts header for previews.
540
+
541
+ Singletons and site settings:
542
+ - 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.
543
+ - get_site_settings/update_site_settings operate on the built-in global site_settings table used by the _site GraphQL query. That surface uses fields like siteName, titleSuffix, fallbackSeoTitle, and fallbackSeoDescription.
544
+
545
+ Asset upload flow:
546
+ Preferred:
547
+ 1. Call import_asset_from_url with a public file URL (normal public redirects are followed automatically)
548
+ 2. The parsed tool payload is the asset object itself; read its top-level id field
549
+ 3. Use that returned asset ID in media/media_gallery fields
550
+
551
+ Manual fallback:
552
+ 1. Upload file to R2 out of band
553
+ 2. Register with upload_asset tool (pass r2Key, filename, mimeType, dimensions)
554
+ 3. Use returned asset ID in media/media_gallery fields
555
+
556
+ Tool argument encoding:
557
+ - Some MCP clients XML-encode tool arguments before they reach the server.
558
+ - 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 build_structured_text code nodes and any create_record/update_record payload carrying code snippets.
560
+
561
+ Raw HTTP / JSON-RPC access:
562
+ - Endpoint: POST <mount>/mcp for admin, POST <mount>/mcp/editor for editor
563
+ - The mount point may be nested (for example /cms/mcp), so scripts should reuse the exact MCP URL already configured in the client instead of assuming root-level /mcp
564
+ - Auth: Authorization: Bearer <token>
565
+ - For standalone curl/HTTP scripts, you can call tools/call directly; do not assume an initialize round-trip is required unless your specific client library expects it
566
+ - Typical tool call body:
567
+ {"jsonrpc":"2.0","method":"tools/call","params":{"name":"create_record","arguments":{...}},"id":1}
568
+ - Tool results usually come back in result.content[0].text as a JSON string payload
569
+ - jq extraction example:
570
+ .result.content[0].text | fromjson
571
+ - 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
+
573
+ Slug fields:
574
+ Set validator {"slug_source": "title"} to auto-generate from a source field.
575
+ Create the slug field AFTER the source field.
576
+
577
+ Pluralization:
578
+ category -> allCategories, blog_post -> allBlogPosts, person -> allPeople
579
+ Powered by standard English pluralization rules.`)
580
+ });
581
+ }
582
+ function createSchemaResource() {
583
+ return McpServer.resource({
584
+ uri: "agent-cms://schema",
585
+ name: "agent-cms-schema",
586
+ description: "Current CMS schema: models, fields, and locales as JSON",
587
+ mimeType: "application/json",
588
+ content: Effect.gen(function* () {
589
+ const sql = yield* SqlClient.SqlClient;
590
+ const models = yield* sql.unsafe("SELECT * FROM models ORDER BY is_block, created_at");
591
+ const fields = yield* sql.unsafe("SELECT * FROM fields ORDER BY model_id, position");
592
+ const locales = yield* sql.unsafe("SELECT * FROM locales ORDER BY position");
593
+ const fieldsByModel = /* @__PURE__ */ new Map();
594
+ for (const f of fields) {
595
+ const list = fieldsByModel.get(f.model_id) ?? [];
596
+ list.push(f);
597
+ fieldsByModel.set(f.model_id, list);
598
+ }
599
+ return encodeJson({
600
+ locales: locales.map((l) => ({
601
+ code: l.code,
602
+ position: l.position,
603
+ fallbackLocaleId: l.fallback_locale_id
604
+ })),
605
+ models: models.map((m) => ({
606
+ id: m.id,
607
+ name: m.name,
608
+ apiKey: m.api_key,
609
+ isBlock: !!m.is_block,
610
+ singleton: !!m.singleton,
611
+ fields: (fieldsByModel.get(m.id) ?? []).map((f) => ({
612
+ id: f.id,
613
+ apiKey: f.api_key,
614
+ label: f.label,
615
+ type: f.field_type,
616
+ localized: !!f.localized,
617
+ validators: parseValidators(f.validators)
618
+ }))
619
+ }))
620
+ });
621
+ })
622
+ });
623
+ }
624
+ function createSetupContentModelPrompt() {
625
+ return McpServer.prompt({
626
+ name: "setup-content-model",
627
+ description: "Guide an agent through designing and creating content models from a description",
628
+ parameters: SetupContentModelPromptInput,
629
+ content: ({ description }) => Effect.succeed(`Set up content models for: ${description}
630
+
631
+ Follow these steps:
632
+ 1. Call schema_info to check existing models — avoid duplicates.
633
+ 2. Design the models and fields needed. Consider:
634
+ - Which are content models vs block types (for StructuredText embedding)?
635
+ - Which fields need slug (add after the source field with slug_source validator)?
636
+ - Which fields reference other models (link/links with item_item_type validator)?
637
+ - Which fields need structured_text (with structured_text_blocks validator for allowed blocks)?
638
+ 3. Present your plan before executing — list models, fields, and relationships.
639
+ 4. Create models first, then fields in order (slug fields after their source).
640
+ 5. Create a few sample records to verify the schema works.
641
+ 6. Publish the sample records with publish_records.
642
+ 7. Show the GraphQL query that a frontend would use to fetch this content.
643
+ Remember: api_key snake_case -> GraphQL camelCase fields, PascalCase types.`)
644
+ });
645
+ }
646
+ function createGenerateGraphqlQueriesPrompt() {
647
+ return McpServer.prompt({
648
+ name: "generate-graphql-queries",
649
+ description: "Generate GraphQL queries for a content model with proper naming conventions",
650
+ parameters: GenerateGraphqlQueriesPromptInput,
651
+ content: ({ modelApiKey }) => Effect.succeed(`Generate GraphQL queries for the "${modelApiKey}" model.
652
+
653
+ Steps:
654
+ 1. Call schema_info with filterByName "${modelApiKey}" to get the full field list.
655
+ 2. Map field names from snake_case (api_key) to camelCase (GraphQL).
656
+ 3. Generate these queries:
657
+ a. List query: all_<pluralized> with pagination, filtering, and ordering
658
+ b. Single query: <model_api_key> by ID or filter
659
+ c. Meta query: _all_<pluralized>_meta for total count
660
+ 4. For each field type, use the right GraphQL fragment:
661
+ - media -> { url width height alt title }
662
+ - structured_text -> { value blocks { ... on <BlockType>Record { id <fields> } } }
663
+ - link -> { id <fields of target model> }
664
+ - links -> same as link but array
665
+ - seo -> { title description image { url } twitterCard }
666
+ - color -> { red green blue alpha hex }
667
+ - lat_lon -> { latitude longitude }
668
+ 5. Include both a "full" query with all fields and a "list" query with essential fields only.`)
669
+ });
670
+ }
671
+ function createMcpLayer(sqlLayer, options) {
672
+ const mode = options?.mode ?? "admin";
673
+ const path = options?.path ?? (mode === "editor" ? "/mcp/editor" : "/mcp");
674
+ const toolkitAny = mode === "editor" ? EditorToolkit : CmsToolkit;
675
+ const defaultVectorizeLayer = Layer.succeed(VectorizeContext, Option.none());
676
+ const defaultHooksLayer = Layer.succeed(HooksContext, Option.none());
677
+ const defaultAssetImportLayer = Layer.succeed(AssetImportContext, {
678
+ r2Bucket: options?.r2Bucket,
679
+ fetch: options?.fetch ?? globalThis.fetch
680
+ });
681
+ const fullLayer = Layer.merge(Layer.merge(Layer.merge(defaultVectorizeLayer, defaultHooksLayer), defaultAssetImportLayer), sqlLayer);
682
+ const serverLayer = McpServer.layerHttpRouter({
683
+ name: mode === "editor" ? "agent-cms-editor" : "agent-cms",
684
+ version: "0.1.0",
685
+ path
686
+ });
687
+ function assetUrl(r2Key) {
688
+ if (!options?.assetBaseUrl) return void 0;
689
+ return `${options.assetBaseUrl.replace(/\/$/, "")}/${r2Key}`;
690
+ }
691
+ function withAssetUrl(asset) {
692
+ const url = assetUrl(asset.r2Key);
693
+ return url ? {
694
+ ...asset,
695
+ url
696
+ } : asset;
697
+ }
698
+ const toolHandlers = {
699
+ schema_info: withDecoded(SchemaInfoInput, ({ filterByName, filterByType, includeFieldDetails }) => Effect.gen(function* () {
700
+ const sql = yield* SqlClient.SqlClient;
701
+ let modelQuery = "SELECT * FROM models";
702
+ const conditions = [];
703
+ const params = [];
704
+ if (filterByType === "model") conditions.push("is_block = 0");
705
+ if (filterByType === "block") conditions.push("is_block = 1");
706
+ if (filterByName) {
707
+ conditions.push("LOWER(name) LIKE ?");
708
+ params.push(`%${filterByName.toLowerCase()}%`);
709
+ }
710
+ if (conditions.length > 0) modelQuery += ` WHERE ${conditions.join(" AND ")}`;
711
+ modelQuery += " ORDER BY is_block, created_at";
712
+ const models = yield* sql.unsafe(modelQuery, params);
713
+ const allFields = yield* sql.unsafe("SELECT * FROM fields ORDER BY model_id, position");
714
+ const locales = yield* sql.unsafe("SELECT * FROM locales ORDER BY position");
715
+ const fieldsByModel = /* @__PURE__ */ new Map();
716
+ for (const f of allFields) {
717
+ const list = fieldsByModel.get(f.model_id) ?? [];
718
+ list.push(f);
719
+ fieldsByModel.set(f.model_id, list);
720
+ }
721
+ return {
722
+ locales: locales.map((l) => ({
723
+ code: l.code,
724
+ position: l.position,
725
+ fallbackLocaleId: l.fallback_locale_id
726
+ })),
727
+ models: models.map((m) => {
728
+ const mFields = fieldsByModel.get(m.id) ?? [];
729
+ return {
730
+ id: m.id,
731
+ name: m.name,
732
+ apiKey: m.api_key,
733
+ isBlock: !!m.is_block,
734
+ singleton: !!m.singleton,
735
+ sortable: !!m.sortable,
736
+ tree: !!m.tree,
737
+ allLocalesRequired: !!m.all_locales_required,
738
+ ...includeFieldDetails ? { fields: mFields.map((f) => ({
739
+ id: f.id,
740
+ label: f.label,
741
+ apiKey: f.api_key,
742
+ type: f.field_type,
743
+ localized: !!f.localized,
744
+ validators: parseValidators(f.validators),
745
+ hint: f.hint
746
+ })) } : {
747
+ fieldCount: mFields.length,
748
+ fieldNames: mFields.map((f) => f.api_key)
749
+ }
750
+ };
751
+ })
752
+ };
753
+ })),
754
+ create_model: withDecoded(CreateModelInput, createModel),
755
+ update_model: withDecoded(UpdateModelInput, ({ modelId, ...rest }) => updateModel(modelId, rest)),
756
+ create_field: withDecoded(Schema.Struct({
757
+ modelId: Schema.String,
758
+ ...CreateFieldInput.fields
759
+ }), ({ modelId, ...rest }) => createField(modelId, rest)),
760
+ update_field: withDecoded(UpdateFieldInput, ({ fieldId, ...rest }) => updateField(fieldId, rest)),
761
+ delete_model: withDecoded(ModelIdInput, ({ modelId }) => deleteModel(modelId)),
762
+ delete_field: withDecoded(FieldIdInput, ({ fieldId }) => deleteField(fieldId)),
763
+ create_record: withDecoded(CreateRecordInput, (input) => createRecord(input, options?.actor)),
764
+ update_record: withDecoded(UpdateRecordInput, ({ recordId, modelApiKey, data }) => {
765
+ if (recordId) return patchRecord(recordId, {
766
+ modelApiKey,
767
+ data
768
+ }, options?.actor);
769
+ return updateSingletonRecord(modelApiKey, data, options?.actor);
770
+ }),
771
+ patch_blocks: withDecoded(PatchBlocksInput, (input) => patchBlocksForField(input, options?.actor)),
772
+ 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)),
775
+ bulk_create_records: withDecoded(BulkCreateRecordsInput, ({ modelApiKey, records }) => bulkCreateRecords({
776
+ modelApiKey,
777
+ records
778
+ }, options?.actor)),
779
+ publish_records: withDecoded(PublishRecordsInput, ({ recordIds, modelApiKey }) => recordIds.length === 1 ? publishRecord(modelApiKey, recordIds[0], options?.actor) : bulkPublishRecords(modelApiKey, recordIds, options?.actor)),
780
+ unpublish_records: withDecoded(PublishRecordsInput, ({ recordIds, modelApiKey }) => recordIds.length === 1 ? unpublishRecord(modelApiKey, recordIds[0], options?.actor) : bulkUnpublishRecords(modelApiKey, recordIds, options?.actor)),
781
+ schedule: withDecoded(ScheduleInput, ({ recordId, modelApiKey, action, at }) => {
782
+ if (action === "clear") return clearSchedule(modelApiKey, recordId, options?.actor);
783
+ if (action === "publish") return schedulePublish(modelApiKey, recordId, at ?? null, options?.actor);
784
+ return scheduleUnpublish(modelApiKey, recordId, at ?? null, options?.actor);
785
+ }),
786
+ record_versions: withDecoded(RecordVersionsInput, ({ action, modelApiKey, recordId, versionId }) => {
787
+ if (action === "list") return listVersions(modelApiKey, recordId);
788
+ if (action === "get") {
789
+ if (!versionId) return Effect.fail({
790
+ _tag: "ValidationError",
791
+ message: "versionId is required for get action"
792
+ });
793
+ return getVersion(versionId);
794
+ }
795
+ if (!versionId) return Effect.fail({
796
+ _tag: "ValidationError",
797
+ message: "versionId is required for restore action"
798
+ });
799
+ return restoreVersion(modelApiKey, recordId, versionId, options?.actor);
800
+ }),
801
+ reorder_records: withDecoded(ReorderInput, ({ modelApiKey, recordIds }) => reorderRecords(modelApiKey, recordIds, options?.actor)),
802
+ remove_block: withDecoded(RemoveBlockInput, ({ blockApiKey, fieldId }) => {
803
+ if (fieldId) return removeBlockFromWhitelist({
804
+ fieldId,
805
+ blockApiKey
806
+ });
807
+ return removeBlockType(blockApiKey);
808
+ }),
809
+ 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
+ upload_asset: withDecoded(CreateAssetInput, (input) => createAsset(input, options?.actor).pipe(Effect.map(withAssetUrl))),
893
+ import_asset_from_url: withDecoded(ImportAssetFromUrlInput, (input) => importAssetFromUrl(input, options?.actor).pipe(Effect.map(withAssetUrl))),
894
+ list_assets: () => listAssets().pipe(Effect.map((assets) => assets.map((a) => {
895
+ const url = assetUrl(a.r2_key);
896
+ return url ? {
897
+ ...a,
898
+ url
899
+ } : a;
900
+ }))),
901
+ replace_asset: withDecoded(ReplaceAssetInput, ({ assetId, ...rest }) => replaceAsset(assetId, rest, options?.actor).pipe(Effect.map(withAssetUrl))),
902
+ schema_io: withDecoded(SchemaIOInput, ({ action, schema }) => {
903
+ if (action === "export") return exportSchema();
904
+ if (!schema) return Effect.fail({
905
+ _tag: "ValidationError",
906
+ message: "schema is required for import action"
907
+ });
908
+ return importSchema(schema);
909
+ }),
910
+ search_content: withDecoded(SearchInput, search),
911
+ reindex_search: withDecoded(ReindexSearchInput, ({ modelApiKey }) => reindexAll(modelApiKey)),
912
+ get_site_settings: () => getSiteSettings(),
913
+ update_site_settings: withDecoded(UpdateSiteSettingsInput, updateSiteSettings),
914
+ editor_tokens: withDecoded(EditorTokensInput, ({ action, name, expiresIn, tokenId }) => {
915
+ if (action === "list") return listEditorTokens();
916
+ if (action === "create") {
917
+ if (!name) return Effect.fail({
918
+ _tag: "ValidationError",
919
+ message: "name is required for create action"
920
+ });
921
+ return createEditorToken({
922
+ name,
923
+ expiresIn
924
+ });
925
+ }
926
+ if (!tokenId) return Effect.fail({
927
+ _tag: "ValidationError",
928
+ message: "tokenId is required for revoke action"
929
+ });
930
+ return revokeEditorToken(tokenId);
931
+ })
932
+ };
933
+ const toolkitHandlers = toolkitAny.toLayer(pickToolkitHandlers(toolkitAny, toolHandlers));
934
+ const toolkitRegistration = Layer.effectDiscard(Effect.gen(function* () {
935
+ const registry = yield* McpServer.McpServer;
936
+ const built = yield* toolkitAny;
937
+ const context = yield* Effect.context();
938
+ for (const tool of Object.values(built.tools)) {
939
+ const mcpTool = new McpSchema.Tool({
940
+ name: tool.name,
941
+ description: tool.description,
942
+ inputSchema: toMcpInputSchema(tool),
943
+ annotations: new McpSchema.ToolAnnotations({
944
+ ...Context.getOption(tool.annotations, AiTool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
945
+ readOnlyHint: Context.get(tool.annotations, AiTool.Readonly),
946
+ destructiveHint: Context.get(tool.annotations, AiTool.Destructive),
947
+ idempotentHint: Context.get(tool.annotations, AiTool.Idempotent),
948
+ openWorldHint: Context.get(tool.annotations, AiTool.OpenWorld)
949
+ })
950
+ });
951
+ yield* registry.addTool({
952
+ tool: mcpTool,
953
+ handle(payload) {
954
+ const params = isToolPayload(payload) ? payload : {};
955
+ return built.handle(tool.name, params).pipe(Effect.provide(context), Effect.match({
956
+ onFailure: (error) => new McpSchema.CallToolResult({
957
+ isError: true,
958
+ structuredContent: toStructuredContent(error),
959
+ content: [{
960
+ type: "text",
961
+ text: encodeJson(error)
962
+ }]
963
+ }),
964
+ onSuccess: (result) => new McpSchema.CallToolResult({
965
+ isError: false,
966
+ structuredContent: toStructuredContent(result.encodedResult),
967
+ content: [{
968
+ type: "text",
969
+ text: encodeJson(result.encodedResult)
970
+ }]
971
+ })
972
+ }));
973
+ }
974
+ });
975
+ }
976
+ })).pipe(Layer.provide(toolkitHandlers));
977
+ const registeredContent = Layer.mergeAll(toolkitRegistration, createGuideResource(), createSchemaResource(), createSetupContentModelPrompt(), createGenerateGraphqlQueriesPrompt()).pipe(Layer.provide(serverLayer));
978
+ return Layer.merge(serverLayer, registeredContent).pipe(Layer.provide(fullLayer));
979
+ }
980
+ //#endregion
981
+ //#region src/mcp/http-transport.ts
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 };
992
+
993
+ //# sourceMappingURL=http-transport-DbFCI6Cs.mjs.map