@content-island/mcp 0.2.2 → 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 +454 -138
- 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: i, location:
|
|
35
|
-
return t && i &&
|
|
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
|
{
|
|
@@ -46,9 +46,9 @@ I'll create a professional ${t} application integrated with your Content Island
|
|
|
46
46
|
**Configuration:**
|
|
47
47
|
- Framework: **${t}**
|
|
48
48
|
- Pages: ${i}
|
|
49
|
-
- Location: ${
|
|
49
|
+
- Location: ${a}
|
|
50
50
|
- Styling: ${o}
|
|
51
|
-
- Design: ${
|
|
51
|
+
- Design: ${s}
|
|
52
52
|
|
|
53
53
|
# CRITICAL IMPLEMENTATION INSTRUCTIONS - FOLLOW EXACTLY
|
|
54
54
|
|
|
@@ -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,10 +140,10 @@ ${t === "Nuxt" ? `
|
|
|
140
140
|
|
|
141
141
|
### STEP 6: PROJECT STRUCTURE GENERATION (FRAMEWORK-SPECIFIC)
|
|
142
142
|
- Framework: ${t}
|
|
143
|
-
- Location: ${
|
|
143
|
+
- Location: ${a}
|
|
144
144
|
- Pages requested: ${i}
|
|
145
145
|
- Styling: ${o}
|
|
146
|
-
- Design approach: ${
|
|
146
|
+
- Design approach: ${s}
|
|
147
147
|
|
|
148
148
|
#### FRAMEWORK-SPECIFIC REQUIREMENTS:
|
|
149
149
|
${t === "Next.js" ? `
|
|
@@ -319,23 +319,23 @@ Please provide all this information so I can create your project.`
|
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
321
|
);
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
},
|
|
322
|
+
}, _ = () => {
|
|
323
|
+
$();
|
|
324
|
+
}, q = {
|
|
325
325
|
UNAUTHORIZED: "Invalid or expired token. Check your CONTENT_ISLAND_ACCESS_TOKEN configuration.",
|
|
326
326
|
FORBIDDEN: "This token does not have write permissions. Configure a write token in CONTENT_ISLAND_ACCESS_TOKEN.",
|
|
327
327
|
NOT_FOUND: "Resource not found. Verify that the content type, content ID, or project exists.",
|
|
328
328
|
VALIDATION_ERROR: "Invalid input. See details below.",
|
|
329
329
|
NETWORK_ERROR: "Could not reach Content Island. Check your network and the API endpoint configuration."
|
|
330
|
-
},
|
|
330
|
+
}, W = (e) => q[e.code] ?? `API error: ${e.status} ${e.code}`, K = (e) => {
|
|
331
331
|
const t = e.details?.fields;
|
|
332
332
|
return Array.isArray(t) ? t.map((i) => ` - ${i.field}: ${i.message}`) : [];
|
|
333
|
-
},
|
|
334
|
-
const t = [
|
|
333
|
+
}, Y = (e) => {
|
|
334
|
+
const t = [W(e), e.message, ...K(e)];
|
|
335
335
|
return e.requestId && t.push(`(requestId: ${e.requestId})`), t.join(`
|
|
336
336
|
`);
|
|
337
|
-
},
|
|
338
|
-
|
|
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(
|
|
339
339
|
"create-content-island-content",
|
|
340
340
|
`Create a new content entry in the Content Island project. Requires a write token.
|
|
341
341
|
|
|
@@ -386,21 +386,21 @@ If the user asks you to create content from a web URL, follow this workflow:
|
|
|
386
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.
|
|
387
387
|
5. Only then call this tool with the fully processed payload.`,
|
|
388
388
|
{
|
|
389
|
-
contentType:
|
|
389
|
+
contentType: n.string().describe(
|
|
390
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.'
|
|
391
391
|
),
|
|
392
|
-
name:
|
|
393
|
-
content:
|
|
394
|
-
|
|
395
|
-
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(
|
|
396
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.`
|
|
397
397
|
),
|
|
398
|
-
fields:
|
|
399
|
-
|
|
400
|
-
name:
|
|
398
|
+
fields: n.array(
|
|
399
|
+
n.object({
|
|
400
|
+
name: n.string().describe(
|
|
401
401
|
"Field name exactly as defined in the content type schema from get-content-island-project."
|
|
402
402
|
),
|
|
403
|
-
value:
|
|
403
|
+
value: n.any().describe(
|
|
404
404
|
"Field value. Must match the type and constraints of the field as described in the tool description above."
|
|
405
405
|
)
|
|
406
406
|
})
|
|
@@ -412,17 +412,207 @@ If the user asks you to create content from a web URL, follow this workflow:
|
|
|
412
412
|
},
|
|
413
413
|
async ({ contentType: e, name: t, content: i }) => {
|
|
414
414
|
try {
|
|
415
|
-
const
|
|
415
|
+
const a = h(), o = { contentType: e, name: t, content: i }, s = await a.createContent(o);
|
|
416
416
|
return {
|
|
417
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
417
|
+
content: [{ type: "text", text: JSON.stringify(s, null, 2) }]
|
|
418
418
|
};
|
|
419
|
-
} catch (
|
|
420
|
-
return
|
|
419
|
+
} catch (a) {
|
|
420
|
+
return p(a);
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
423
|
);
|
|
424
|
-
},
|
|
425
|
-
|
|
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);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
}, se = () => {
|
|
615
|
+
m.tool(
|
|
426
616
|
"get-content-island-project",
|
|
427
617
|
`Get the user's Content Island project schema (languages and contentTypes).
|
|
428
618
|
|
|
@@ -439,14 +629,14 @@ Each entity field also carries a "validations" array (optional). Each entry is {
|
|
|
439
629
|
|
|
440
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).`,
|
|
441
631
|
async () => {
|
|
442
|
-
const t = await
|
|
632
|
+
const t = await h().getProject();
|
|
443
633
|
return {
|
|
444
634
|
content: [{ type: "text", text: JSON.stringify(t, null, 2) }]
|
|
445
635
|
};
|
|
446
636
|
}
|
|
447
637
|
);
|
|
448
|
-
},
|
|
449
|
-
|
|
638
|
+
}, T = 25, v = 100, le = () => {
|
|
639
|
+
m.tool(
|
|
450
640
|
"list-content-island-contents",
|
|
451
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.
|
|
452
642
|
|
|
@@ -454,7 +644,7 @@ IMPORTANT: Before calling this tool, you SHOULD call get-content-island-project
|
|
|
454
644
|
|
|
455
645
|
Pagination:
|
|
456
646
|
- The response always includes { items, skip, take, hasMore }.
|
|
457
|
-
- Default take is ${
|
|
647
|
+
- Default take is ${T}, maximum allowed is ${v}.
|
|
458
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).
|
|
459
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.
|
|
460
650
|
|
|
@@ -471,100 +661,100 @@ Sort (optional): object with any of:
|
|
|
471
661
|
- contentType: "asc" | "desc"
|
|
472
662
|
- lastUpdate: "asc" | "desc"`,
|
|
473
663
|
{
|
|
474
|
-
contentType:
|
|
475
|
-
id:
|
|
476
|
-
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(
|
|
477
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.'
|
|
478
668
|
),
|
|
479
|
-
status:
|
|
669
|
+
status: n.union([n.enum(["draft", "changed", "published"]), n.array(n.enum(["draft", "changed", "published"]))]).optional().describe(
|
|
480
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.'
|
|
481
671
|
),
|
|
482
|
-
includeRelatedContent:
|
|
483
|
-
sort:
|
|
484
|
-
contentType:
|
|
485
|
-
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()
|
|
486
676
|
}).optional().describe('Sort order. Each field is independently set to "asc" or "desc".'),
|
|
487
|
-
take:
|
|
488
|
-
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.")
|
|
489
679
|
},
|
|
490
|
-
async ({ contentType: e, id: t, language: i, status:
|
|
680
|
+
async ({ contentType: e, id: t, language: i, status: a, includeRelatedContent: o, sort: s, take: r, skip: d }) => {
|
|
491
681
|
try {
|
|
492
|
-
const l =
|
|
493
|
-
pagination: { take: c, skip:
|
|
682
|
+
const l = h(), c = r ?? T, f = d ?? 0, g = {
|
|
683
|
+
pagination: { take: c, skip: f }
|
|
494
684
|
};
|
|
495
|
-
e !== void 0 && (
|
|
496
|
-
const
|
|
497
|
-
items:
|
|
498
|
-
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,
|
|
499
689
|
take: c,
|
|
500
|
-
hasMore:
|
|
690
|
+
hasMore: u.length === c
|
|
501
691
|
};
|
|
502
692
|
return {
|
|
503
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
693
|
+
content: [{ type: "text", text: JSON.stringify(y, null, 2) }]
|
|
504
694
|
};
|
|
505
695
|
} catch (l) {
|
|
506
|
-
return
|
|
696
|
+
return p(l);
|
|
507
697
|
}
|
|
508
698
|
}
|
|
509
699
|
);
|
|
510
|
-
},
|
|
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) => {
|
|
511
701
|
const t = e.lastIndexOf(".");
|
|
512
702
|
return t === -1 ? null : e.slice(t);
|
|
513
|
-
},
|
|
703
|
+
}, ue = (e, t) => {
|
|
514
704
|
const i = typeof e == "string" ? e : e?.url;
|
|
515
705
|
if (typeof i != "string")
|
|
516
706
|
return !1;
|
|
517
|
-
const
|
|
518
|
-
return
|
|
519
|
-
},
|
|
707
|
+
const a = de(i);
|
|
708
|
+
return a !== null && t.includes(a);
|
|
709
|
+
}, pe = (e, t, i) => {
|
|
520
710
|
switch (e.name) {
|
|
521
711
|
case "required":
|
|
522
712
|
case "unique":
|
|
523
713
|
return null;
|
|
524
714
|
case "min-length": {
|
|
525
|
-
const
|
|
526
|
-
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;
|
|
527
717
|
}
|
|
528
718
|
case "max-length": {
|
|
529
|
-
const
|
|
530
|
-
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;
|
|
531
721
|
}
|
|
532
722
|
case "media-type": {
|
|
533
|
-
const
|
|
534
|
-
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;
|
|
535
725
|
}
|
|
536
726
|
default:
|
|
537
727
|
return null;
|
|
538
728
|
}
|
|
539
|
-
},
|
|
540
|
-
const i = (e.contentTypes ?? []).find((
|
|
729
|
+
}, me = (e, t) => {
|
|
730
|
+
const i = (e.contentTypes ?? []).find((r) => ce(r) && r.name === t.contentType.name);
|
|
541
731
|
if (!i)
|
|
542
732
|
return {
|
|
543
733
|
ok: !1,
|
|
544
734
|
errors: [`content type "${t.contentType.name}" not found in project schema`]
|
|
545
735
|
};
|
|
546
|
-
const
|
|
547
|
-
for (const
|
|
548
|
-
if (
|
|
736
|
+
const a = [], o = e.languages ?? [], s = t.fields ?? [];
|
|
737
|
+
for (const r of i.fields)
|
|
738
|
+
if (r.isRequired)
|
|
549
739
|
for (const d of o) {
|
|
550
|
-
const l =
|
|
551
|
-
(!l || N(l.value)) &&
|
|
740
|
+
const l = s.find((c) => c.name === r.name && c.language === d);
|
|
741
|
+
(!l || N(l.value)) && a.push(`required field "${r.name}" is empty in language "${d}"`);
|
|
552
742
|
}
|
|
553
|
-
for (const
|
|
554
|
-
const d = i.fields.find((l) => l.name ===
|
|
555
|
-
if (!(!d || N(
|
|
743
|
+
for (const r of s) {
|
|
744
|
+
const d = i.fields.find((l) => l.name === r.name);
|
|
745
|
+
if (!(!d || N(r.value)))
|
|
556
746
|
for (const l of d.validations ?? []) {
|
|
557
|
-
const c =
|
|
558
|
-
c &&
|
|
747
|
+
const c = pe(l, r.value, d);
|
|
748
|
+
c && a.push(`field "${r.name}" [${r.language}]: ${c}`);
|
|
559
749
|
}
|
|
560
750
|
}
|
|
561
|
-
return
|
|
562
|
-
},
|
|
751
|
+
return a.length === 0 ? { ok: !0 } : { ok: !1, errors: a };
|
|
752
|
+
}, he = (e) => `Cannot publish content — preflight validation failed:
|
|
563
753
|
${e.map((t) => ` - ${t}`).join(`
|
|
564
754
|
`)}
|
|
565
755
|
|
|
566
|
-
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.`,
|
|
567
|
-
|
|
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(
|
|
568
758
|
"publish-content-island-content",
|
|
569
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.
|
|
570
760
|
|
|
@@ -592,30 +782,30 @@ Errors:
|
|
|
592
782
|
- 404 if the contentId does not exist.
|
|
593
783
|
- 401/403 if the configured token is not a write token.`,
|
|
594
784
|
{
|
|
595
|
-
contentId:
|
|
785
|
+
contentId: n.string().describe("The id of the existing content entry to publish.")
|
|
596
786
|
},
|
|
597
787
|
async ({ contentId: e }) => {
|
|
598
788
|
try {
|
|
599
|
-
const t =
|
|
600
|
-
if (!
|
|
789
|
+
const t = h(), [i, a] = await Promise.all([t.getProject(), t.getRawContent({ id: e })]);
|
|
790
|
+
if (!a)
|
|
601
791
|
return {
|
|
602
792
|
content: [{ type: "text", text: `Content "${e}" not found.` }],
|
|
603
793
|
isError: !0
|
|
604
794
|
};
|
|
605
|
-
const o =
|
|
795
|
+
const o = me(i, a);
|
|
606
796
|
return o.ok === !1 ? {
|
|
607
|
-
content: [{ type: "text", text:
|
|
797
|
+
content: [{ type: "text", text: he(o.errors) }],
|
|
608
798
|
isError: !0
|
|
609
799
|
} : (await t.publishContent(e), {
|
|
610
800
|
content: [{ type: "text", text: JSON.stringify({ contentId: e, status: "published" }, null, 2) }]
|
|
611
801
|
});
|
|
612
802
|
} catch (t) {
|
|
613
|
-
return
|
|
803
|
+
return p(t);
|
|
614
804
|
}
|
|
615
805
|
}
|
|
616
806
|
);
|
|
617
|
-
},
|
|
618
|
-
|
|
807
|
+
}, fe = () => {
|
|
808
|
+
m.tool(
|
|
619
809
|
"update-content-island-field-value",
|
|
620
810
|
`Update or add a single field value of an existing content in the Content Island project. Requires a write token.
|
|
621
811
|
|
|
@@ -657,26 +847,152 @@ Fields whose "type" contains "|" (e.g. "abc123|Foo") are either ENUMS or RELATIO
|
|
|
657
847
|
|
|
658
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.`,
|
|
659
849
|
{
|
|
660
|
-
contentId:
|
|
661
|
-
fieldName:
|
|
662
|
-
language:
|
|
663
|
-
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(
|
|
664
854
|
"The new field value. Must match the type and isArray of the field as described in the tool description above."
|
|
665
855
|
)
|
|
666
856
|
},
|
|
667
|
-
async ({ contentId: e, fieldName: t, language: i, value:
|
|
857
|
+
async ({ contentId: e, fieldName: t, language: i, value: a }) => {
|
|
668
858
|
try {
|
|
669
|
-
return await
|
|
859
|
+
return await h().updateContentFieldValue(e, { fieldName: t, language: i }, a), {
|
|
670
860
|
content: [
|
|
671
861
|
{ type: "text", text: JSON.stringify({ contentId: e, fieldName: t, language: i, status: "updated" }, null, 2) }
|
|
672
862
|
]
|
|
673
863
|
};
|
|
674
864
|
} catch (o) {
|
|
675
|
-
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);
|
|
676
992
|
}
|
|
677
993
|
}
|
|
678
994
|
);
|
|
679
|
-
},
|
|
995
|
+
}, Ie = {
|
|
680
996
|
".png": "image/png",
|
|
681
997
|
".jpg": "image/jpeg",
|
|
682
998
|
".jpeg": "image/jpeg",
|
|
@@ -690,42 +1006,42 @@ The tool always identifies the field by its name within the content type. If the
|
|
|
690
1006
|
".mov": "video/quicktime",
|
|
691
1007
|
".mp3": "audio/mpeg",
|
|
692
1008
|
".wav": "audio/wav"
|
|
693
|
-
},
|
|
1009
|
+
}, be = (e) => /^https?:\/\//i.test(e), we = async (e) => {
|
|
694
1010
|
const t = await fetch(e);
|
|
695
1011
|
if (!t.ok)
|
|
696
1012
|
throw new Error(`Failed to fetch ${e}: ${t.status} ${t.statusText}`);
|
|
697
|
-
const i = await t.blob(),
|
|
1013
|
+
const i = await t.blob(), a = new URL(e).pathname, o = C(a) || "download";
|
|
698
1014
|
return { blob: i, fileName: o };
|
|
699
|
-
},
|
|
700
|
-
const t = await
|
|
701
|
-
return { blob: o, fileName:
|
|
702
|
-
},
|
|
703
|
-
|
|
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);
|
|
1017
|
+
return { blob: o, fileName: s };
|
|
1018
|
+
}, Ae = () => {
|
|
1019
|
+
m.tool(
|
|
704
1020
|
"upload-content-island-media",
|
|
705
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.",
|
|
706
1022
|
{
|
|
707
|
-
source:
|
|
1023
|
+
source: n.string().describe(
|
|
708
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.'
|
|
709
1025
|
),
|
|
710
|
-
fileName:
|
|
1026
|
+
fileName: n.string().optional().describe(
|
|
711
1027
|
"Override the file name used in Content Island. Defaults to the file name from the source path or URL."
|
|
712
1028
|
)
|
|
713
1029
|
},
|
|
714
1030
|
async ({ source: e, fileName: t }) => {
|
|
715
1031
|
try {
|
|
716
|
-
const { blob: i, fileName:
|
|
1032
|
+
const { blob: i, fileName: a } = be(e) ? await we(e) : await Ne(e), s = await h().uploadMedia({ file: i, fileName: t ?? a });
|
|
717
1033
|
return {
|
|
718
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
1034
|
+
content: [{ type: "text", text: JSON.stringify(s, null, 2) }]
|
|
719
1035
|
};
|
|
720
1036
|
} catch (i) {
|
|
721
|
-
return
|
|
1037
|
+
return p(i);
|
|
722
1038
|
}
|
|
723
1039
|
}
|
|
724
1040
|
);
|
|
725
|
-
},
|
|
726
|
-
|
|
1041
|
+
}, Ce = () => {
|
|
1042
|
+
se(), le(), B(), fe(), ge(), Ae(), ie(), H(), ve(), ye(), re(), oe();
|
|
727
1043
|
};
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
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
|
},
|