@content-island/mcp 0.2.1 → 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 +14 -8
- package/dist/index.js +465 -161
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -16,14 +16,20 @@ A Model Context Protocol (MCP) server that enables seamless integration between
|
|
|
16
16
|
|
|
17
17
|
### Available Tools
|
|
18
18
|
|
|
19
|
-
| Tool | Description
|
|
20
|
-
| ----------------------------------- |
|
|
21
|
-
| `get-content-island-project` | Get project details (languages, content types, fields)
|
|
22
|
-
| `list-content-island-contents` | List content entries with filters, sort and pagination (returns hasMore)
|
|
23
|
-
| `create-content-island-content` | Create a new content entry with fields per language
|
|
24
|
-
| `update-content-island-field-value` | Update or insert a field value identified by `(fieldName, language)`
|
|
25
|
-
| `publish-content-island-content` | Promote a content's current draft to the live state (preflight-gated)
|
|
26
|
-
| `upload-content-island-media` | Upload a media file from a local path or URL
|
|
19
|
+
| Tool | Description | Token required |
|
|
20
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
|
21
|
+
| `get-content-island-project` | Get project details (languages, content types, fields) | Read **or** Write |
|
|
22
|
+
| `list-content-island-contents` | List content entries with filters, sort and pagination (returns hasMore) | Read **or** Write |
|
|
23
|
+
| `create-content-island-content` | Create a new content entry with fields per language | **Write** |
|
|
24
|
+
| `update-content-island-field-value` | Update or insert a field value identified by `(fieldName, language)` | **Write** |
|
|
25
|
+
| `publish-content-island-content` | Promote a content's current draft to the live state (preflight-gated) | **Write** |
|
|
26
|
+
| `upload-content-island-media` | Upload a media file from a local path or URL | **Write** |
|
|
27
|
+
| `create-content-island-model` | Create an Entity (content type) with a structured field list | **Write** |
|
|
28
|
+
| `update-content-island-model` | Edit an Entity with declarative ops (rename / add / update / remove fields); destructive removals are dry-run-gated | **Write** |
|
|
29
|
+
| `delete-content-island-model` | Delete an Entity (dry-run by default; `confirm: true` to apply; 409 if referenced) | **Write** |
|
|
30
|
+
| `create-content-island-enum` | Create an Enum (a closed list of string values) | **Write** |
|
|
31
|
+
| `update-content-island-enum` | Edit an Enum with declarative ops (rename / add / rename / remove values); destructive removals are dry-run-gated | **Write** |
|
|
32
|
+
| `delete-content-island-enum` | Delete an Enum (dry-run by default; `confirm: true` to apply; 409 if referenced) | **Write** |
|
|
27
33
|
|
|
28
34
|
> **Note**: A **read token** is sufficient for read-only tools. Tools that create or upload data require a **write token**. A write token also works for read-only tools.
|
|
29
35
|
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { z as
|
|
2
|
-
import { McpServer as
|
|
3
|
-
import { createClient as
|
|
4
|
-
import { readFile as
|
|
5
|
-
import { basename as
|
|
6
|
-
import { StdioServerTransport as
|
|
7
|
-
const
|
|
1
|
+
import { z as n } from "zod";
|
|
2
|
+
import { McpServer as S } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { createClient as L, isApiClientError as j, resolveFileExtension as k } from "@content-island/api-client";
|
|
4
|
+
import { readFile as F } from "node:fs/promises";
|
|
5
|
+
import { basename as C, extname as U } from "node:path";
|
|
6
|
+
import { StdioServerTransport as D } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
const E = {
|
|
8
8
|
CONTENT_ISLAND_ACCESS_TOKEN: process.env.CONTENT_ISLAND_ACCESS_TOKEN,
|
|
9
9
|
CONTENT_ISLAND_DOMAIN: process.env.CONTENT_ISLAND_DOMAIN,
|
|
10
10
|
CONTENT_ISLAND_SECURE_PROTOCOL: process.env.CONTENT_ISLAND_SECURE_PROTOCOL !== "false",
|
|
11
11
|
CONTENT_ISLAND_API_VERSION: process.env.CONTENT_ISLAND_API_VERSION
|
|
12
|
-
},
|
|
13
|
-
accessToken:
|
|
14
|
-
domain:
|
|
15
|
-
secureProtocol:
|
|
16
|
-
apiVersion:
|
|
17
|
-
}),
|
|
18
|
-
version:
|
|
19
|
-
},
|
|
12
|
+
}, w = "PREVIEW_", M = (e) => e.startsWith(w) ? e : `${w}${e}`, h = () => L({
|
|
13
|
+
accessToken: M(E.CONTENT_ISLAND_ACCESS_TOKEN),
|
|
14
|
+
domain: E.CONTENT_ISLAND_DOMAIN,
|
|
15
|
+
secureProtocol: E.CONTENT_ISLAND_SECURE_PROTOCOL,
|
|
16
|
+
apiVersion: E.CONTENT_ISLAND_API_VERSION
|
|
17
|
+
}), P = "0.3.0", V = {
|
|
18
|
+
version: P
|
|
19
|
+
}, m = new S({
|
|
20
20
|
name: "Content Island",
|
|
21
|
-
version:
|
|
22
|
-
}),
|
|
23
|
-
|
|
21
|
+
version: V.version
|
|
22
|
+
}), $ = () => {
|
|
23
|
+
m.prompt(
|
|
24
24
|
"create-content-island-project",
|
|
25
25
|
"Professional MCP Server prompt for creating modern frontend projects integrated with Content Island CMS",
|
|
26
26
|
{
|
|
27
|
-
framework:
|
|
28
|
-
pages:
|
|
29
|
-
location:
|
|
30
|
-
styling:
|
|
31
|
-
design:
|
|
27
|
+
framework: n.string().describe("Framework choice (Next.js, Astro, Nuxt, etc.)"),
|
|
28
|
+
pages: n.string().describe("Pages needed (Homepage, Blog, Contact, etc.)"),
|
|
29
|
+
location: n.string().describe("Project location (root directory or subfolder name)"),
|
|
30
|
+
styling: n.string().describe("Styling preference (Tailwind CSS or custom)"),
|
|
31
|
+
design: n.string().describe("Design assets (mockups, wireframes, or none)")
|
|
32
32
|
},
|
|
33
33
|
async (e) => {
|
|
34
|
-
const { framework: t, pages:
|
|
35
|
-
return t &&
|
|
34
|
+
const { framework: t, pages: i, location: a, styling: o, design: s } = e;
|
|
35
|
+
return t && i && a && o && s ? {
|
|
36
36
|
description: "Content Island project generator",
|
|
37
37
|
messages: [
|
|
38
38
|
{
|
|
@@ -45,8 +45,8 @@ I'll create a professional ${t} application integrated with your Content Island
|
|
|
45
45
|
|
|
46
46
|
**Configuration:**
|
|
47
47
|
- Framework: **${t}**
|
|
48
|
-
- Pages: ${
|
|
49
|
-
- Location: ${
|
|
48
|
+
- Pages: ${i}
|
|
49
|
+
- Location: ${a}
|
|
50
50
|
- Styling: ${o}
|
|
51
51
|
- Design: ${s}
|
|
52
52
|
|
|
@@ -58,7 +58,7 @@ You are a specialized web development assistant creating a ${t} application inte
|
|
|
58
58
|
|
|
59
59
|
### STEP 1: FRAMEWORK PROJECT INITIALIZATION (REQUIRED FIRST ACTION)
|
|
60
60
|
- FIRST execute the appropriate framework CLI command to create the base project:
|
|
61
|
-
${t === "Next.js" ? ` * Run: npx create-next-app@latest ${
|
|
61
|
+
${t === "Next.js" ? ` * Run: npx create-next-app@latest ${a !== "root directory" ? a : "."} --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"` : t === "Nuxt" ? ` * Run: npx nuxi@latest init ${a !== "root directory" ? a : "."}` : t === "Astro" ? ` * Run: npm create astro@latest ${a !== "root directory" ? a : "."}` : t === "SvelteKit" ? ` * Run: npm create svelte@latest ${a !== "root directory" ? a : "."}` : t === "Vite" ? ` * Run: npm create vite@latest ${a !== "root directory" ? a : "."} -- --template react-ts` : ` * Run the appropriate CLI command for ${t} to create a new project in ${a !== "root directory" ? a : "current directory"}`}
|
|
62
62
|
- Navigate to the project directory if created in a subfolder
|
|
63
63
|
- Wait for the framework setup to complete before proceeding
|
|
64
64
|
- DO NOT skip this step - the framework base is required for all subsequent steps
|
|
@@ -140,8 +140,8 @@ ${t === "Nuxt" ? `
|
|
|
140
140
|
|
|
141
141
|
### STEP 6: PROJECT STRUCTURE GENERATION (FRAMEWORK-SPECIFIC)
|
|
142
142
|
- Framework: ${t}
|
|
143
|
-
- Location: ${
|
|
144
|
-
- Pages requested: ${
|
|
143
|
+
- Location: ${a}
|
|
144
|
+
- Pages requested: ${i}
|
|
145
145
|
- Styling: ${o}
|
|
146
146
|
- Design approach: ${s}
|
|
147
147
|
|
|
@@ -273,7 +273,7 @@ ${t === "Nuxt" ? `
|
|
|
273
273
|
□ All content types have corresponding API functions using official client
|
|
274
274
|
`}
|
|
275
275
|
□ TypeScript interfaces exactly match project structure
|
|
276
|
-
□ All requested pages (${
|
|
276
|
+
□ All requested pages (${i}) are implemented
|
|
277
277
|
□ Error handling implemented for all API calls
|
|
278
278
|
□ Loading states implemented for all data fetching
|
|
279
279
|
□ Responsive design implemented
|
|
@@ -319,35 +319,23 @@ Please provide all this information so I can create your project.`
|
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
321
|
);
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
},
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
${a.join(`
|
|
340
|
-
`)}` : t;
|
|
341
|
-
}, h = (e) => {
|
|
342
|
-
let t = e instanceof Error ? e.message : String(e);
|
|
343
|
-
try {
|
|
344
|
-
const a = JSON.parse(t);
|
|
345
|
-
t = M(a);
|
|
346
|
-
} catch {
|
|
347
|
-
}
|
|
348
|
-
return { content: [{ type: "text", text: t }], isError: !0 };
|
|
349
|
-
}, F = () => {
|
|
350
|
-
u.tool(
|
|
322
|
+
}, _ = () => {
|
|
323
|
+
$();
|
|
324
|
+
}, q = {
|
|
325
|
+
UNAUTHORIZED: "Invalid or expired token. Check your CONTENT_ISLAND_ACCESS_TOKEN configuration.",
|
|
326
|
+
FORBIDDEN: "This token does not have write permissions. Configure a write token in CONTENT_ISLAND_ACCESS_TOKEN.",
|
|
327
|
+
NOT_FOUND: "Resource not found. Verify that the content type, content ID, or project exists.",
|
|
328
|
+
VALIDATION_ERROR: "Invalid input. See details below.",
|
|
329
|
+
NETWORK_ERROR: "Could not reach Content Island. Check your network and the API endpoint configuration."
|
|
330
|
+
}, W = (e) => q[e.code] ?? `API error: ${e.status} ${e.code}`, K = (e) => {
|
|
331
|
+
const t = e.details?.fields;
|
|
332
|
+
return Array.isArray(t) ? t.map((i) => ` - ${i.field}: ${i.message}`) : [];
|
|
333
|
+
}, Y = (e) => {
|
|
334
|
+
const t = [W(e), e.message, ...K(e)];
|
|
335
|
+
return e.requestId && t.push(`(requestId: ${e.requestId})`), t.join(`
|
|
336
|
+
`);
|
|
337
|
+
}, p = (e) => j(e) ? { content: [{ type: "text", text: Y(e) }], isError: !0 } : { content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }], isError: !0 }, B = () => {
|
|
338
|
+
m.tool(
|
|
351
339
|
"create-content-island-content",
|
|
352
340
|
`Create a new content entry in the Content Island project. Requires a write token.
|
|
353
341
|
|
|
@@ -398,21 +386,21 @@ If the user asks you to create content from a web URL, follow this workflow:
|
|
|
398
386
|
4. Replace every original image URL in the extracted content with the "url" returned by upload-content-island-media. Never leave the original external URLs — they may become broken links if the source site removes them.
|
|
399
387
|
5. Only then call this tool with the fully processed payload.`,
|
|
400
388
|
{
|
|
401
|
-
contentType:
|
|
389
|
+
contentType: n.string().describe(
|
|
402
390
|
'The content type name (e.g. "post", "page"). Must match an existing content type name exactly. Use get-content-island-project to discover available content types.'
|
|
403
391
|
),
|
|
404
|
-
name:
|
|
405
|
-
content:
|
|
406
|
-
|
|
407
|
-
language:
|
|
392
|
+
name: n.string().describe("Display name for the new content entry."),
|
|
393
|
+
content: n.array(
|
|
394
|
+
n.object({
|
|
395
|
+
language: n.string().optional().describe(
|
|
408
396
|
`Language code (e.g. "en", "es"). Must be one of the languages from the project. Defaults to the project's first language if omitted.`
|
|
409
397
|
),
|
|
410
|
-
fields:
|
|
411
|
-
|
|
412
|
-
name:
|
|
398
|
+
fields: n.array(
|
|
399
|
+
n.object({
|
|
400
|
+
name: n.string().describe(
|
|
413
401
|
"Field name exactly as defined in the content type schema from get-content-island-project."
|
|
414
402
|
),
|
|
415
|
-
value:
|
|
403
|
+
value: n.any().describe(
|
|
416
404
|
"Field value. Must match the type and constraints of the field as described in the tool description above."
|
|
417
405
|
)
|
|
418
406
|
})
|
|
@@ -422,19 +410,209 @@ If the user asks you to create content from a web URL, follow this workflow:
|
|
|
422
410
|
"Content entries per language. Provide one entry per language in the project. If omitted, creates an empty content with no field values."
|
|
423
411
|
)
|
|
424
412
|
},
|
|
425
|
-
async ({ contentType: e, name: t, content:
|
|
413
|
+
async ({ contentType: e, name: t, content: i }) => {
|
|
426
414
|
try {
|
|
427
|
-
const
|
|
415
|
+
const a = h(), o = { contentType: e, name: t, content: i }, s = await a.createContent(o);
|
|
428
416
|
return {
|
|
429
417
|
content: [{ type: "text", text: JSON.stringify(s, null, 2) }]
|
|
430
418
|
};
|
|
431
|
-
} catch (
|
|
432
|
-
return
|
|
419
|
+
} catch (a) {
|
|
420
|
+
return p(a);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}, H = () => {
|
|
425
|
+
m.tool(
|
|
426
|
+
"create-content-island-enum",
|
|
427
|
+
`Create a new Enum model in the Content Island project schema. Requires a write token.
|
|
428
|
+
|
|
429
|
+
An enum is a closed list of string values that entity fields (type "enum") can reference. The server generates an id for the enum and for each value.
|
|
430
|
+
|
|
431
|
+
WORKFLOW:
|
|
432
|
+
1. Call get-content-island-project to avoid duplicate model names.
|
|
433
|
+
2. Call this tool with the enum name and its list of string values.`,
|
|
434
|
+
{
|
|
435
|
+
name: n.string().describe(
|
|
436
|
+
"Enum name. Non-empty, /^[a-zA-Z0-9_ -]+$/, max 128 chars, not a reserved field type, unique in the project."
|
|
437
|
+
),
|
|
438
|
+
values: n.array(n.string()).min(1).describe("The enum values (plain strings), in order. At least one value is required.")
|
|
439
|
+
},
|
|
440
|
+
async ({ name: e, values: t }) => {
|
|
441
|
+
try {
|
|
442
|
+
const i = h(), a = { name: e, values: t.map((s) => ({ value: s })) }, o = await i.createEnum(a);
|
|
443
|
+
return { content: [{ type: "text", text: JSON.stringify(o, null, 2) }] };
|
|
444
|
+
} catch (i) {
|
|
445
|
+
return p(i);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
}, b = n.discriminatedUnion("name", [
|
|
450
|
+
n.object({ name: n.literal("required") }),
|
|
451
|
+
n.object({ name: n.literal("unique") }),
|
|
452
|
+
n.object({ name: n.literal("min-length"), customArgs: n.object({ length: n.number().int().positive() }) }),
|
|
453
|
+
n.object({ name: n.literal("max-length"), customArgs: n.object({ length: n.number().int().positive() }) }),
|
|
454
|
+
n.object({
|
|
455
|
+
name: n.literal("media-type"),
|
|
456
|
+
customArgs: n.object({
|
|
457
|
+
allowedExtensions: n.array(n.string()).min(1).describe('Allowed file extensions, e.g. ["png","jpg","pdf"] — with or without the leading dot.')
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
]).describe(
|
|
461
|
+
'Field validation. required/unique take no args; min-length/max-length take { length }; media-type takes { allowedExtensions: ["png", ...] }.'
|
|
462
|
+
), I = (e) => (e ?? []).map((t) => t.name === "media-type" ? {
|
|
463
|
+
name: "media-type",
|
|
464
|
+
customArgs: { allowedExtensions: t.customArgs.allowedExtensions.map(k) }
|
|
465
|
+
} : "customArgs" in t ? { name: t.name, customArgs: t.customArgs } : { name: t.name }), G = (e) => {
|
|
466
|
+
const t = e.type, i = {
|
|
467
|
+
id: e.id,
|
|
468
|
+
name: e.name,
|
|
469
|
+
isArray: e.isArray ?? !1,
|
|
470
|
+
validations: e.validations ?? []
|
|
471
|
+
};
|
|
472
|
+
if (!t.includes("|"))
|
|
473
|
+
return { ...i, type: t };
|
|
474
|
+
const a = t.split("|")[0], o = (e.tsType ?? "").trim().startsWith("'") ? "enum" : "relation";
|
|
475
|
+
return { ...i, type: o, relatedModelId: a };
|
|
476
|
+
}, J = (e) => ({
|
|
477
|
+
name: e.name,
|
|
478
|
+
type: e.type,
|
|
479
|
+
relatedModelId: e.relatedModelId,
|
|
480
|
+
isArray: e.isArray ?? !1,
|
|
481
|
+
validations: e.validations ?? []
|
|
482
|
+
}), z = (e, t) => ({
|
|
483
|
+
id: e.id,
|
|
484
|
+
name: t.name ?? e.name,
|
|
485
|
+
type: t.type ?? e.type,
|
|
486
|
+
relatedModelId: t.relatedModelId ?? e.relatedModelId,
|
|
487
|
+
isArray: t.isArray ?? e.isArray,
|
|
488
|
+
validations: t.validations ?? e.validations
|
|
489
|
+
}), X = (e, t) => {
|
|
490
|
+
const i = new Set(t.removeFieldIds ?? []), a = new Map((t.updateFields ?? []).map((r) => [r.id, r])), o = e.fields.filter((r) => !i.has(r.id)).map((r) => {
|
|
491
|
+
const d = G(r), l = a.get(r.id);
|
|
492
|
+
return l ? z(d, l) : d;
|
|
493
|
+
}), s = (t.addFields ?? []).map(J);
|
|
494
|
+
return {
|
|
495
|
+
name: t.rename ?? e.name,
|
|
496
|
+
fieldList: [...o, ...s]
|
|
497
|
+
};
|
|
498
|
+
}, Q = (e, t) => {
|
|
499
|
+
const i = new Set(t.removeValueIds ?? []), a = new Map((t.renameValues ?? []).map((r) => [r.id, r.value])), o = e.values.filter((r) => !i.has(r.id)).map((r) => ({ id: r.id, value: a.get(r.id) ?? r.value })), s = (t.addValues ?? []).map((r) => ({ value: r }));
|
|
500
|
+
return {
|
|
501
|
+
name: t.rename ?? e.name,
|
|
502
|
+
values: [...o, ...s]
|
|
503
|
+
};
|
|
504
|
+
}, Z = (e, t) => ({
|
|
505
|
+
wouldRemoveFieldIds: [...t.removeFieldIds ?? []]
|
|
506
|
+
}), ee = (e, t) => ({
|
|
507
|
+
wouldRemoveValueIds: [...t.removeValueIds ?? []]
|
|
508
|
+
}), te = (e) => (e.removeFieldIds ?? []).length > 0, ne = (e) => (e.removeValueIds ?? []).length > 0, R = (e, t) => (e ?? []).find((i) => i.type === "entity" && i.id === t), x = (e, t) => (e ?? []).find((i) => i.type === "enum" && i.id === t), ae = n.object({
|
|
509
|
+
name: n.string().describe("Field name. Must match /^[a-zA-Z0-9_ -]+$/ and be unique within the model."),
|
|
510
|
+
type: n.enum(["short-text", "long-text", "number", "date", "date-time", "media", "boolean", "color", "relation", "enum"]).describe(
|
|
511
|
+
'Field type. Use a primitive type for plain fields. Use "relation" to point at another entity (supply relatedModelId) and "enum" to point at an enum (supply relatedModelId). Never build a composite "modelId|Name" type string.'
|
|
512
|
+
),
|
|
513
|
+
relatedModelId: n.string().optional().describe(
|
|
514
|
+
'For type "relation" or "enum": the 24-char id of the referenced model (an entity for "relation", an enum for "enum"). Discover ids via get-content-island-project. Omit for primitive types.'
|
|
515
|
+
),
|
|
516
|
+
isArray: n.boolean().optional().describe("Whether the field stores a list of values. Defaults to false."),
|
|
517
|
+
validations: n.array(b).optional().describe("Optional field validations.")
|
|
518
|
+
}), ie = () => {
|
|
519
|
+
m.tool(
|
|
520
|
+
"create-content-island-model",
|
|
521
|
+
`Create a new Entity model (content type) in the Content Island project schema. Requires a write token.
|
|
522
|
+
|
|
523
|
+
The agent supplies a structured field list. For "relation"/"enum" fields, pass the referenced model's id as relatedModelId — the server resolves it and assembles the stored type. Never construct a "modelId|Name" string yourself.
|
|
524
|
+
|
|
525
|
+
WORKFLOW:
|
|
526
|
+
1. Call get-content-island-project to read existing models (so you can reference entities/enums by id and avoid duplicate names).
|
|
527
|
+
2. Call this tool with the new model name and its fieldList.`,
|
|
528
|
+
{
|
|
529
|
+
name: n.string().describe(
|
|
530
|
+
"Model (entity) name. Non-empty, /^[a-zA-Z0-9_ -]+$/, max 128 chars, not a reserved field type, unique in the project."
|
|
531
|
+
),
|
|
532
|
+
fieldList: n.array(ae).min(1).describe("The fields of the new entity, in order. At least one field is required.")
|
|
533
|
+
},
|
|
534
|
+
async ({ name: e, fieldList: t }) => {
|
|
535
|
+
try {
|
|
536
|
+
const i = h(), a = {
|
|
537
|
+
name: e,
|
|
538
|
+
fieldList: t.map((s) => ({
|
|
539
|
+
...s,
|
|
540
|
+
validations: I(s.validations)
|
|
541
|
+
}))
|
|
542
|
+
}, o = await i.createModel(a);
|
|
543
|
+
return { content: [{ type: "text", text: JSON.stringify(o, null, 2) }] };
|
|
544
|
+
} catch (i) {
|
|
545
|
+
return p(i);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}, oe = () => {
|
|
550
|
+
m.tool(
|
|
551
|
+
"delete-content-island-enum",
|
|
552
|
+
`Delete an Enum from the Content Island project schema. Requires a write token.
|
|
553
|
+
|
|
554
|
+
DESTRUCTIVE: without confirm: true, this tool returns a DRY-RUN and does NOT mutate. To apply, do it in TWO steps: first call WITHOUT confirm to get the dry-run, then show the user that impact and re-invoke with confirm: true ONLY after the user explicitly approves. NEVER pass confirm: true on the first call or without the user's explicit go-ahead.
|
|
555
|
+
|
|
556
|
+
If any entity field references this enum, the delete is rejected with a 409 CONFLICT and the enum is NOT deleted — there is no force override. Remove or repoint the referencing field first.`,
|
|
557
|
+
{
|
|
558
|
+
enumId: n.string().describe("The 24-char id of the enum to delete."),
|
|
559
|
+
confirm: n.boolean().optional().describe(
|
|
560
|
+
"Set to true to perform the deletion. Without it, returns a dry-run. Set it only AFTER the user has seen the dry-run and explicitly approved — never on the first call."
|
|
561
|
+
)
|
|
562
|
+
},
|
|
563
|
+
async ({ enumId: e, confirm: t }) => {
|
|
564
|
+
try {
|
|
565
|
+
const i = h(), a = await i.getProject(), o = x(a.contentTypes, e);
|
|
566
|
+
if (!o)
|
|
567
|
+
return p(new Error(`Enum "${e}" not found in the project schema.`));
|
|
568
|
+
if (!t) {
|
|
569
|
+
const s = {
|
|
570
|
+
dryRun: !0,
|
|
571
|
+
summary: `Deleting the enum "${o.name}" is destructive. Re-invoke with confirm: true to apply. (If an entity field references this enum, the deletion will be rejected with 409.)`
|
|
572
|
+
};
|
|
573
|
+
return { content: [{ type: "text", text: JSON.stringify(s, null, 2) }] };
|
|
574
|
+
}
|
|
575
|
+
return await i.deleteEnum(e), { content: [{ type: "text", text: `Enum "${o.name}" (${e}) deleted.` }] };
|
|
576
|
+
} catch (i) {
|
|
577
|
+
return p(i);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
}, re = () => {
|
|
582
|
+
m.tool(
|
|
583
|
+
"delete-content-island-model",
|
|
584
|
+
`Delete an Entity model from the Content Island project schema. Requires a write token.
|
|
585
|
+
|
|
586
|
+
DESTRUCTIVE: deleting a model also deletes every content of that type. Without confirm: true, this tool returns a DRY-RUN reporting how many contents would be deleted and does NOT mutate. To apply, do it in TWO steps: first call WITHOUT confirm to get the dry-run, then show the user that impact and re-invoke with confirm: true ONLY after the user explicitly approves. NEVER pass confirm: true on the first call or without the user's explicit go-ahead.
|
|
587
|
+
|
|
588
|
+
If the model is referenced by a field on another model, the delete is rejected with a 409 CONFLICT and the model is NOT deleted — there is no force override. Remove or repoint the referencing field first.`,
|
|
589
|
+
{
|
|
590
|
+
modelId: n.string().describe("The 24-char id of the entity to delete."),
|
|
591
|
+
confirm: n.boolean().optional().describe(
|
|
592
|
+
"Set to true to perform the deletion. Without it, returns a dry-run with the cascade impact. Set it only AFTER the user has seen the dry-run and explicitly approved — never on the first call."
|
|
593
|
+
)
|
|
594
|
+
},
|
|
595
|
+
async ({ modelId: e, confirm: t }) => {
|
|
596
|
+
try {
|
|
597
|
+
const i = h(), a = await i.getProject(), o = R(a.contentTypes, e);
|
|
598
|
+
if (!o)
|
|
599
|
+
return p(new Error(`Entity "${e}" not found in the project schema.`));
|
|
600
|
+
if (!t) {
|
|
601
|
+
const s = await i.getContentListSize({ contentType: o.name }), r = {
|
|
602
|
+
dryRun: !0,
|
|
603
|
+
wouldDeleteContentCount: s,
|
|
604
|
+
summary: `Deleting "${o.name}" would also delete ${s} content(s) of this type. Re-invoke with confirm: true to apply. (If another model references this one, the deletion will be rejected with 409.)`
|
|
605
|
+
};
|
|
606
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
607
|
+
}
|
|
608
|
+
return await i.deleteModel(e), { content: [{ type: "text", text: `Model "${o.name}" (${e}) deleted.` }] };
|
|
609
|
+
} catch (i) {
|
|
610
|
+
return p(i);
|
|
433
611
|
}
|
|
434
612
|
}
|
|
435
613
|
);
|
|
436
|
-
},
|
|
437
|
-
|
|
614
|
+
}, se = () => {
|
|
615
|
+
m.tool(
|
|
438
616
|
"get-content-island-project",
|
|
439
617
|
`Get the user's Content Island project schema (languages and contentTypes).
|
|
440
618
|
|
|
@@ -451,14 +629,14 @@ Each entity field also carries a "validations" array (optional). Each entry is {
|
|
|
451
629
|
|
|
452
630
|
The "languages" array at the project root lists every language the project supports. Field values are stored per language, so a single content entry may have one fieldValue per language. When writing, consider language coverage explicitly (see the write tools' descriptions).`,
|
|
453
631
|
async () => {
|
|
454
|
-
const t = await
|
|
632
|
+
const t = await h().getProject();
|
|
455
633
|
return {
|
|
456
634
|
content: [{ type: "text", text: JSON.stringify(t, null, 2) }]
|
|
457
635
|
};
|
|
458
636
|
}
|
|
459
637
|
);
|
|
460
|
-
},
|
|
461
|
-
|
|
638
|
+
}, T = 25, v = 100, le = () => {
|
|
639
|
+
m.tool(
|
|
462
640
|
"list-content-island-contents",
|
|
463
641
|
`List content entries of the Content Island project. Returns the raw content structure with all fieldValues (each one carries its fieldName, language and value), so you can inspect every translation in one call. Drafts and unpublished values are included.
|
|
464
642
|
|
|
@@ -466,7 +644,7 @@ IMPORTANT: Before calling this tool, you SHOULD call get-content-island-project
|
|
|
466
644
|
|
|
467
645
|
Pagination:
|
|
468
646
|
- The response always includes { items, skip, take, hasMore }.
|
|
469
|
-
- Default take is ${
|
|
647
|
+
- Default take is ${T}, maximum allowed is ${v}.
|
|
470
648
|
- If hasMore is true, call this tool again with skip = previous skip + previous take to fetch the next page. Repeat until hasMore is false (or items is empty).
|
|
471
649
|
- hasMore is computed as items.length === take, so it can occasionally be a false positive when the total happens to be an exact multiple of take; in that case the next call returns an empty items array, which confirms the end of the list.
|
|
472
650
|
|
|
@@ -483,100 +661,100 @@ Sort (optional): object with any of:
|
|
|
483
661
|
- contentType: "asc" | "desc"
|
|
484
662
|
- lastUpdate: "asc" | "desc"`,
|
|
485
663
|
{
|
|
486
|
-
contentType:
|
|
487
|
-
id:
|
|
488
|
-
language:
|
|
664
|
+
contentType: n.string().optional().describe("Filter by content type name. Must match an existing content type from get-content-island-project."),
|
|
665
|
+
id: n.union([n.string(), n.array(n.string())]).optional().describe("A single content id, or an array of ids to fetch a specific subset of contents."),
|
|
666
|
+
language: n.string().optional().describe(
|
|
489
667
|
'Language code (e.g. "en", "es"). Restricts which fieldValues are included inside each content. Does not remove contents that lack a translation in the given language.'
|
|
490
668
|
),
|
|
491
|
-
status:
|
|
669
|
+
status: n.union([n.enum(["draft", "changed", "published"]), n.array(n.enum(["draft", "changed", "published"]))]).optional().describe(
|
|
492
670
|
'Filter by publication state. Pass a single value (e.g. "draft") or an array (e.g. ["draft", "changed"]) to find contents with unpublished data.'
|
|
493
671
|
),
|
|
494
|
-
includeRelatedContent:
|
|
495
|
-
sort:
|
|
496
|
-
contentType:
|
|
497
|
-
lastUpdate:
|
|
672
|
+
includeRelatedContent: n.boolean().optional().describe("When true, related content references are expanded inline in fieldValues."),
|
|
673
|
+
sort: n.object({
|
|
674
|
+
contentType: n.enum(["asc", "desc"]).optional(),
|
|
675
|
+
lastUpdate: n.enum(["asc", "desc"]).optional()
|
|
498
676
|
}).optional().describe('Sort order. Each field is independently set to "asc" or "desc".'),
|
|
499
|
-
take:
|
|
500
|
-
skip:
|
|
677
|
+
take: n.number().int().min(1).max(v).optional().describe(`Maximum number of items to return. Default ${T}, maximum ${v}.`),
|
|
678
|
+
skip: n.number().int().min(0).optional().describe("Number of items to skip from the start of the result set. Use for pagination.")
|
|
501
679
|
},
|
|
502
|
-
async ({ contentType: e, id: t, language:
|
|
680
|
+
async ({ contentType: e, id: t, language: i, status: a, includeRelatedContent: o, sort: s, take: r, skip: d }) => {
|
|
503
681
|
try {
|
|
504
|
-
const l =
|
|
505
|
-
pagination: { take: c, skip:
|
|
682
|
+
const l = h(), c = r ?? T, f = d ?? 0, g = {
|
|
683
|
+
pagination: { take: c, skip: f }
|
|
506
684
|
};
|
|
507
|
-
e !== void 0 && (
|
|
508
|
-
const
|
|
509
|
-
items:
|
|
510
|
-
skip:
|
|
685
|
+
e !== void 0 && (g.contentType = e), t !== void 0 && (g.id = Array.isArray(t) ? { in: t } : t), i !== void 0 && (g.language = i), a !== void 0 && (g.status = Array.isArray(a) ? { in: a } : a), o !== void 0 && (g.includeRelatedContent = o), s !== void 0 && (g.sort = s);
|
|
686
|
+
const u = await l.getRawContentList(g), y = {
|
|
687
|
+
items: u,
|
|
688
|
+
skip: f,
|
|
511
689
|
take: c,
|
|
512
|
-
hasMore:
|
|
690
|
+
hasMore: u.length === c
|
|
513
691
|
};
|
|
514
692
|
return {
|
|
515
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
693
|
+
content: [{ type: "text", text: JSON.stringify(y, null, 2) }]
|
|
516
694
|
};
|
|
517
695
|
} catch (l) {
|
|
518
|
-
return
|
|
696
|
+
return p(l);
|
|
519
697
|
}
|
|
520
698
|
}
|
|
521
699
|
);
|
|
522
|
-
},
|
|
700
|
+
}, ce = (e) => e?.type === "entity", N = (e) => !!(e == null || typeof e == "string" && e === "" || Array.isArray(e) && e.length === 0), A = (e, t) => t && Array.isArray(e) || typeof e == "string" ? e.length : null, de = (e) => {
|
|
523
701
|
const t = e.lastIndexOf(".");
|
|
524
702
|
return t === -1 ? null : e.slice(t);
|
|
525
|
-
},
|
|
526
|
-
const
|
|
527
|
-
if (typeof
|
|
703
|
+
}, ue = (e, t) => {
|
|
704
|
+
const i = typeof e == "string" ? e : e?.url;
|
|
705
|
+
if (typeof i != "string")
|
|
528
706
|
return !1;
|
|
529
|
-
const
|
|
530
|
-
return
|
|
531
|
-
},
|
|
707
|
+
const a = de(i);
|
|
708
|
+
return a !== null && t.includes(a);
|
|
709
|
+
}, pe = (e, t, i) => {
|
|
532
710
|
switch (e.name) {
|
|
533
711
|
case "required":
|
|
534
712
|
case "unique":
|
|
535
713
|
return null;
|
|
536
714
|
case "min-length": {
|
|
537
|
-
const
|
|
538
|
-
return
|
|
715
|
+
const a = A(t, i.isArray), o = e.customArgs?.length;
|
|
716
|
+
return a === null || typeof o != "number" ? null : a < o ? `expected min length ${o}, got ${a}` : null;
|
|
539
717
|
}
|
|
540
718
|
case "max-length": {
|
|
541
|
-
const
|
|
542
|
-
return
|
|
719
|
+
const a = A(t, i.isArray), o = e.customArgs?.length;
|
|
720
|
+
return a === null || typeof o != "number" ? null : a > o ? `expected max length ${o}, got ${a}` : null;
|
|
543
721
|
}
|
|
544
722
|
case "media-type": {
|
|
545
|
-
const
|
|
546
|
-
return
|
|
723
|
+
const a = (e.customArgs?.allowedExtensions ?? []).map((r) => r.name);
|
|
724
|
+
return a.length === 0 ? null : (i.isArray && Array.isArray(t) ? t : [t]).some((r) => !ue(r, a)) ? `media file extension not in allowed list (${a.join(", ")})` : null;
|
|
547
725
|
}
|
|
548
726
|
default:
|
|
549
727
|
return null;
|
|
550
728
|
}
|
|
551
|
-
},
|
|
552
|
-
const
|
|
553
|
-
if (!
|
|
729
|
+
}, me = (e, t) => {
|
|
730
|
+
const i = (e.contentTypes ?? []).find((r) => ce(r) && r.name === t.contentType.name);
|
|
731
|
+
if (!i)
|
|
554
732
|
return {
|
|
555
733
|
ok: !1,
|
|
556
734
|
errors: [`content type "${t.contentType.name}" not found in project schema`]
|
|
557
735
|
};
|
|
558
|
-
const
|
|
559
|
-
for (const r of
|
|
736
|
+
const a = [], o = e.languages ?? [], s = t.fields ?? [];
|
|
737
|
+
for (const r of i.fields)
|
|
560
738
|
if (r.isRequired)
|
|
561
739
|
for (const d of o) {
|
|
562
740
|
const l = s.find((c) => c.name === r.name && c.language === d);
|
|
563
|
-
(!l ||
|
|
741
|
+
(!l || N(l.value)) && a.push(`required field "${r.name}" is empty in language "${d}"`);
|
|
564
742
|
}
|
|
565
743
|
for (const r of s) {
|
|
566
|
-
const d =
|
|
567
|
-
if (!(!d ||
|
|
744
|
+
const d = i.fields.find((l) => l.name === r.name);
|
|
745
|
+
if (!(!d || N(r.value)))
|
|
568
746
|
for (const l of d.validations ?? []) {
|
|
569
|
-
const c =
|
|
570
|
-
c &&
|
|
747
|
+
const c = pe(l, r.value, d);
|
|
748
|
+
c && a.push(`field "${r.name}" [${r.language}]: ${c}`);
|
|
571
749
|
}
|
|
572
750
|
}
|
|
573
|
-
return
|
|
574
|
-
},
|
|
751
|
+
return a.length === 0 ? { ok: !0 } : { ok: !1, errors: a };
|
|
752
|
+
}, he = (e) => `Cannot publish content — preflight validation failed:
|
|
575
753
|
${e.map((t) => ` - ${t}`).join(`
|
|
576
754
|
`)}
|
|
577
755
|
|
|
578
|
-
Fix the issues (e.g. via update-content-island-field-value) and retry. Use list-content-island-contents with the content id to inspect the current draft state.`,
|
|
579
|
-
|
|
756
|
+
Fix the issues (e.g. via update-content-island-field-value) and retry. Use list-content-island-contents with the content id to inspect the current draft state.`, ge = () => {
|
|
757
|
+
m.tool(
|
|
580
758
|
"publish-content-island-content",
|
|
581
759
|
`Publish an existing content in the Content Island project. This promotes the current draft state to the live state visible to consumers without the PREVIEW_ token prefix. Requires a write token.
|
|
582
760
|
|
|
@@ -604,30 +782,30 @@ Errors:
|
|
|
604
782
|
- 404 if the contentId does not exist.
|
|
605
783
|
- 401/403 if the configured token is not a write token.`,
|
|
606
784
|
{
|
|
607
|
-
contentId:
|
|
785
|
+
contentId: n.string().describe("The id of the existing content entry to publish.")
|
|
608
786
|
},
|
|
609
787
|
async ({ contentId: e }) => {
|
|
610
788
|
try {
|
|
611
|
-
const t =
|
|
612
|
-
if (!
|
|
789
|
+
const t = h(), [i, a] = await Promise.all([t.getProject(), t.getRawContent({ id: e })]);
|
|
790
|
+
if (!a)
|
|
613
791
|
return {
|
|
614
792
|
content: [{ type: "text", text: `Content "${e}" not found.` }],
|
|
615
793
|
isError: !0
|
|
616
794
|
};
|
|
617
|
-
const o =
|
|
795
|
+
const o = me(i, a);
|
|
618
796
|
return o.ok === !1 ? {
|
|
619
|
-
content: [{ type: "text", text:
|
|
797
|
+
content: [{ type: "text", text: he(o.errors) }],
|
|
620
798
|
isError: !0
|
|
621
799
|
} : (await t.publishContent(e), {
|
|
622
800
|
content: [{ type: "text", text: JSON.stringify({ contentId: e, status: "published" }, null, 2) }]
|
|
623
801
|
});
|
|
624
802
|
} catch (t) {
|
|
625
|
-
return
|
|
803
|
+
return p(t);
|
|
626
804
|
}
|
|
627
805
|
}
|
|
628
806
|
);
|
|
629
|
-
},
|
|
630
|
-
|
|
807
|
+
}, fe = () => {
|
|
808
|
+
m.tool(
|
|
631
809
|
"update-content-island-field-value",
|
|
632
810
|
`Update or add a single field value of an existing content in the Content Island project. Requires a write token.
|
|
633
811
|
|
|
@@ -669,26 +847,152 @@ Fields whose "type" contains "|" (e.g. "abc123|Foo") are either ENUMS or RELATIO
|
|
|
669
847
|
|
|
670
848
|
The tool always identifies the field by its name within the content type. If the field is renamed in the schema between calls, this tool will return a 404; refresh the schema with get-content-island-project and retry.`,
|
|
671
849
|
{
|
|
672
|
-
contentId:
|
|
673
|
-
fieldName:
|
|
674
|
-
language:
|
|
675
|
-
value:
|
|
850
|
+
contentId: n.string().describe("The id of the existing content entry to update."),
|
|
851
|
+
fieldName: n.string().describe("Name of the field as defined in the content type schema (from get-content-island-project)."),
|
|
852
|
+
language: n.string().describe('Language code of the field value (e.g. "en", "es"). Must be one of the project languages.'),
|
|
853
|
+
value: n.any().describe(
|
|
676
854
|
"The new field value. Must match the type and isArray of the field as described in the tool description above."
|
|
677
855
|
)
|
|
678
856
|
},
|
|
679
|
-
async ({ contentId: e, fieldName: t, language:
|
|
857
|
+
async ({ contentId: e, fieldName: t, language: i, value: a }) => {
|
|
680
858
|
try {
|
|
681
|
-
return await
|
|
859
|
+
return await h().updateContentFieldValue(e, { fieldName: t, language: i }, a), {
|
|
682
860
|
content: [
|
|
683
|
-
{ type: "text", text: JSON.stringify({ contentId: e, fieldName: t, language:
|
|
861
|
+
{ type: "text", text: JSON.stringify({ contentId: e, fieldName: t, language: i, status: "updated" }, null, 2) }
|
|
684
862
|
]
|
|
685
863
|
};
|
|
686
864
|
} catch (o) {
|
|
687
|
-
return
|
|
865
|
+
return p(o);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
}, ye = () => {
|
|
870
|
+
m.tool(
|
|
871
|
+
"update-content-island-enum",
|
|
872
|
+
`Edit an existing Enum using DECLARATIVE operations. The tool reads the current enum and applies your changes on top, preserving untouched values (and their ids) automatically.
|
|
873
|
+
|
|
874
|
+
Operations (all optional, combine freely):
|
|
875
|
+
- rename: new enum name.
|
|
876
|
+
- addValues: new string values to append.
|
|
877
|
+
- renameValues: change existing values by id ({ id, value }). Renaming propagates the new label into existing content.
|
|
878
|
+
- removeValueIds: ids of values to delete.
|
|
879
|
+
|
|
880
|
+
DESTRUCTIVE GUARD: removing a value also clears it from every existing content using it. If removeValueIds is non-empty and you do NOT pass confirm: true, the tool returns a DRY-RUN and does NOT mutate. To apply, do it in TWO steps: first call WITHOUT confirm to get the dry-run, then show the user that impact and re-invoke with confirm: true ONLY after the user explicitly approves. NEVER pass confirm: true on the first call or without the user's explicit go-ahead.`,
|
|
881
|
+
{
|
|
882
|
+
enumId: n.string().describe("The 24-char id of the enum to update."),
|
|
883
|
+
rename: n.string().optional().describe("New enum name."),
|
|
884
|
+
addValues: n.array(n.string()).optional().describe("Values to add (appended)."),
|
|
885
|
+
renameValues: n.array(n.object({ id: n.string(), value: n.string() })).optional().describe("Existing values to rename, keyed by id."),
|
|
886
|
+
removeValueIds: n.array(n.string()).optional().describe("Ids of values to remove. DESTRUCTIVE — requires confirm: true."),
|
|
887
|
+
confirm: n.boolean().optional().describe(
|
|
888
|
+
"Set to true to apply a destructive change (value removal). Without it, a destructive update returns a dry-run. Set it only AFTER the user has seen the dry-run and explicitly approved — never on the first call."
|
|
889
|
+
)
|
|
890
|
+
},
|
|
891
|
+
async ({ enumId: e, rename: t, addValues: i, renameValues: a, removeValueIds: o, confirm: s }) => {
|
|
892
|
+
try {
|
|
893
|
+
const r = h(), d = await r.getProject(), l = x(d.contentTypes, e);
|
|
894
|
+
if (!l)
|
|
895
|
+
return p(new Error(`Enum "${e}" not found in the project schema.`));
|
|
896
|
+
const c = {
|
|
897
|
+
rename: t,
|
|
898
|
+
addValues: i,
|
|
899
|
+
renameValues: a,
|
|
900
|
+
removeValueIds: o
|
|
901
|
+
};
|
|
902
|
+
if (ne(c) && !s) {
|
|
903
|
+
const u = ee(l, c), y = {
|
|
904
|
+
dryRun: !0,
|
|
905
|
+
wouldRemoveValueIds: u.wouldRemoveValueIds,
|
|
906
|
+
summary: `This update would remove ${u.wouldRemoveValueIds.length} value(s) from "${l.name}" and clear them from every existing content. Re-invoke with confirm: true to apply.`
|
|
907
|
+
};
|
|
908
|
+
return { content: [{ type: "text", text: JSON.stringify(y, null, 2) }] };
|
|
909
|
+
}
|
|
910
|
+
const f = Q(l, c), g = await r.updateEnum(e, f);
|
|
911
|
+
return { content: [{ type: "text", text: JSON.stringify(g, null, 2) }] };
|
|
912
|
+
} catch (r) {
|
|
913
|
+
return p(r);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
}, O = n.enum([
|
|
918
|
+
"short-text",
|
|
919
|
+
"long-text",
|
|
920
|
+
"number",
|
|
921
|
+
"date",
|
|
922
|
+
"date-time",
|
|
923
|
+
"media",
|
|
924
|
+
"boolean",
|
|
925
|
+
"color",
|
|
926
|
+
"relation",
|
|
927
|
+
"enum"
|
|
928
|
+
]), Ee = n.object({
|
|
929
|
+
name: n.string(),
|
|
930
|
+
type: O,
|
|
931
|
+
relatedModelId: n.string().optional(),
|
|
932
|
+
isArray: n.boolean().optional(),
|
|
933
|
+
validations: n.array(b).optional()
|
|
934
|
+
}), Te = n.object({
|
|
935
|
+
id: n.string().describe("The existing field id to modify (from get-content-island-project)."),
|
|
936
|
+
name: n.string().optional(),
|
|
937
|
+
type: O.optional(),
|
|
938
|
+
relatedModelId: n.string().optional(),
|
|
939
|
+
isArray: n.boolean().optional(),
|
|
940
|
+
validations: n.array(b).optional()
|
|
941
|
+
}), ve = () => {
|
|
942
|
+
m.tool(
|
|
943
|
+
"update-content-island-model",
|
|
944
|
+
`Edit an existing Entity model using DECLARATIVE operations. The tool reads the current model and applies your changes on top, so you NEVER hand-build the full field list and untouched fields are preserved automatically.
|
|
945
|
+
|
|
946
|
+
Operations (all optional, combine freely):
|
|
947
|
+
- rename: new model name.
|
|
948
|
+
- addFields: new fields to append.
|
|
949
|
+
- updateFields: change existing fields by id (name/type/relatedModelId/isArray/validations).
|
|
950
|
+
- removeFieldIds: ids of fields to delete.
|
|
951
|
+
|
|
952
|
+
DESTRUCTIVE GUARD: removing a field also deletes its values from every existing content. If removeFieldIds is non-empty and you do NOT pass confirm: true, the tool returns a DRY-RUN describing the impact and does NOT mutate. To apply, do it in TWO steps: first call WITHOUT confirm to get the dry-run, then show the user that impact and re-invoke with confirm: true ONLY after the user explicitly approves. NEVER pass confirm: true on the first call or without the user's explicit go-ahead.`,
|
|
953
|
+
{
|
|
954
|
+
modelId: n.string().describe("The 24-char id of the entity to update."),
|
|
955
|
+
rename: n.string().optional().describe("New model name."),
|
|
956
|
+
addFields: n.array(Ee).optional().describe("Fields to add (appended after existing fields)."),
|
|
957
|
+
updateFields: n.array(Te).optional().describe("Existing fields to modify, keyed by id."),
|
|
958
|
+
removeFieldIds: n.array(n.string()).optional().describe("Ids of fields to remove. DESTRUCTIVE — requires confirm: true."),
|
|
959
|
+
confirm: n.boolean().optional().describe(
|
|
960
|
+
"Set to true to apply a destructive change (field removal). Without it, a destructive update returns a dry-run. Set it only AFTER the user has seen the dry-run and explicitly approved — never on the first call."
|
|
961
|
+
)
|
|
962
|
+
},
|
|
963
|
+
async ({ modelId: e, rename: t, addFields: i, updateFields: a, removeFieldIds: o, confirm: s }) => {
|
|
964
|
+
try {
|
|
965
|
+
const r = h(), d = await r.getProject(), l = R(d.contentTypes, e);
|
|
966
|
+
if (!l)
|
|
967
|
+
return p(new Error(`Entity "${e}" not found in the project schema.`));
|
|
968
|
+
const c = {
|
|
969
|
+
rename: t,
|
|
970
|
+
addFields: i?.map((u) => ({
|
|
971
|
+
...u,
|
|
972
|
+
validations: I(u.validations)
|
|
973
|
+
})),
|
|
974
|
+
updateFields: a?.map((u) => ({
|
|
975
|
+
...u,
|
|
976
|
+
validations: u.validations ? I(u.validations) : void 0
|
|
977
|
+
})),
|
|
978
|
+
removeFieldIds: o
|
|
979
|
+
};
|
|
980
|
+
if (te(c) && !s) {
|
|
981
|
+
const u = Z(l, c), y = {
|
|
982
|
+
dryRun: !0,
|
|
983
|
+
wouldRemoveFieldIds: u.wouldRemoveFieldIds,
|
|
984
|
+
summary: `This update would remove ${u.wouldRemoveFieldIds.length} field(s) from "${l.name}" and delete their values from every existing content. Re-invoke with confirm: true to apply.`
|
|
985
|
+
};
|
|
986
|
+
return { content: [{ type: "text", text: JSON.stringify(y, null, 2) }] };
|
|
987
|
+
}
|
|
988
|
+
const f = X(l, c), g = await r.updateModel(e, f);
|
|
989
|
+
return { content: [{ type: "text", text: JSON.stringify(g, null, 2) }] };
|
|
990
|
+
} catch (r) {
|
|
991
|
+
return p(r);
|
|
688
992
|
}
|
|
689
993
|
}
|
|
690
994
|
);
|
|
691
|
-
},
|
|
995
|
+
}, Ie = {
|
|
692
996
|
".png": "image/png",
|
|
693
997
|
".jpg": "image/jpeg",
|
|
694
998
|
".jpeg": "image/jpeg",
|
|
@@ -702,42 +1006,42 @@ The tool always identifies the field by its name within the content type. If the
|
|
|
702
1006
|
".mov": "video/quicktime",
|
|
703
1007
|
".mp3": "audio/mpeg",
|
|
704
1008
|
".wav": "audio/wav"
|
|
705
|
-
},
|
|
1009
|
+
}, be = (e) => /^https?:\/\//i.test(e), we = async (e) => {
|
|
706
1010
|
const t = await fetch(e);
|
|
707
1011
|
if (!t.ok)
|
|
708
1012
|
throw new Error(`Failed to fetch ${e}: ${t.status} ${t.statusText}`);
|
|
709
|
-
const
|
|
710
|
-
return { blob:
|
|
711
|
-
},
|
|
712
|
-
const t = await
|
|
1013
|
+
const i = await t.blob(), a = new URL(e).pathname, o = C(a) || "download";
|
|
1014
|
+
return { blob: i, fileName: o };
|
|
1015
|
+
}, Ne = async (e) => {
|
|
1016
|
+
const t = await F(e), i = U(e).toLowerCase(), a = Ie[i] ?? "application/octet-stream", o = new Blob([t], { type: a }), s = C(e);
|
|
713
1017
|
return { blob: o, fileName: s };
|
|
714
|
-
},
|
|
715
|
-
|
|
1018
|
+
}, Ae = () => {
|
|
1019
|
+
m.tool(
|
|
716
1020
|
"upload-content-island-media",
|
|
717
1021
|
"Upload a media file to the Content Island project. Accepts a local file path or a URL (http/https). Requires a write token. Note: file attachments in the chat cannot be uploaded directly — provide the file path on disk or a public URL instead.",
|
|
718
1022
|
{
|
|
719
|
-
source:
|
|
1023
|
+
source: n.string().describe(
|
|
720
1024
|
'Path to a local file or a URL (http/https) pointing to the media to upload. Examples: "./assets/hero.png", "https://example.com/photo.jpg". File attachments in the conversation cannot be used directly — ask the user for the file path or URL.'
|
|
721
1025
|
),
|
|
722
|
-
fileName:
|
|
1026
|
+
fileName: n.string().optional().describe(
|
|
723
1027
|
"Override the file name used in Content Island. Defaults to the file name from the source path or URL."
|
|
724
1028
|
)
|
|
725
1029
|
},
|
|
726
1030
|
async ({ source: e, fileName: t }) => {
|
|
727
1031
|
try {
|
|
728
|
-
const { blob:
|
|
1032
|
+
const { blob: i, fileName: a } = be(e) ? await we(e) : await Ne(e), s = await h().uploadMedia({ file: i, fileName: t ?? a });
|
|
729
1033
|
return {
|
|
730
1034
|
content: [{ type: "text", text: JSON.stringify(s, null, 2) }]
|
|
731
1035
|
};
|
|
732
|
-
} catch (
|
|
733
|
-
return
|
|
1036
|
+
} catch (i) {
|
|
1037
|
+
return p(i);
|
|
734
1038
|
}
|
|
735
1039
|
}
|
|
736
1040
|
);
|
|
737
|
-
},
|
|
738
|
-
|
|
1041
|
+
}, Ce = () => {
|
|
1042
|
+
se(), le(), B(), fe(), ge(), Ae(), ie(), H(), ve(), ye(), re(), oe();
|
|
739
1043
|
};
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
await
|
|
1044
|
+
_();
|
|
1045
|
+
Ce();
|
|
1046
|
+
const Re = new D();
|
|
1047
|
+
await m.connect(Re);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@content-island/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Content Island - MCP (Model Context Protocol) server",
|
|
5
5
|
"private": false,
|
|
6
6
|
"sideEffects": false,
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "vite build",
|
|
32
32
|
"debug": "npm run build && npx @modelcontextprotocol/inspector node ./dist/index",
|
|
33
|
-
"type-check": "tsc --noEmit --preserveWatchOutput"
|
|
33
|
+
"type-check": "tsc --noEmit --preserveWatchOutput",
|
|
34
|
+
"test": "vitest run -c ./config/test/config.ts",
|
|
35
|
+
"test:watch": "vitest -c ./config/test/config.ts"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@content-island/api-client": "0.
|
|
38
|
+
"@content-island/api-client": "0.22.0",
|
|
37
39
|
"@modelcontextprotocol/sdk": "1.13.3",
|
|
38
40
|
"zod": "3.25.71"
|
|
39
41
|
},
|