@content-island/mcp 0.2.2 → 0.3.1

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.
Files changed (3) hide show
  1. package/README.md +14 -8
  2. package/dist/index.js +454 -138
  3. 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 | 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** |
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 a } from "zod";
2
- import { McpServer as w } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { createClient as v, isApiClientError as R } from "@content-island/api-client";
4
- import { readFile as S } from "node:fs/promises";
5
- import { basename as A, extname as O } from "node:path";
6
- import { StdioServerTransport as x } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- const f = {
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
- }, T = "PREVIEW_", L = (e) => e.startsWith(T) ? e : `${T}${e}`, g = () => v({
13
- accessToken: L(f.CONTENT_ISLAND_ACCESS_TOKEN),
14
- domain: f.CONTENT_ISLAND_DOMAIN,
15
- secureProtocol: f.CONTENT_ISLAND_SECURE_PROTOCOL,
16
- apiVersion: f.CONTENT_ISLAND_API_VERSION
17
- }), k = "0.2.2", P = {
18
- version: k
19
- }, u = new w({
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.1", V = {
18
+ version: P
19
+ }, m = new S({
20
20
  name: "Content Island",
21
- version: P.version
22
- }), j = () => {
23
- u.prompt(
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: a.string().describe("Framework choice (Next.js, Astro, Nuxt, etc.)"),
28
- pages: a.string().describe("Pages needed (Homepage, Blog, Contact, etc.)"),
29
- location: a.string().describe("Project location (root directory or subfolder name)"),
30
- styling: a.string().describe("Styling preference (Tailwind CSS or custom)"),
31
- design: a.string().describe("Design assets (mockups, wireframes, or none)")
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: n, styling: o, design: r } = e;
35
- return t && i && n && o && r ? {
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: ${n}
49
+ - Location: ${a}
50
50
  - Styling: ${o}
51
- - Design: ${r}
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 ${n !== "root directory" ? n : "."} --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"` : t === "Nuxt" ? ` * Run: npx nuxi@latest init ${n !== "root directory" ? n : "."}` : t === "Astro" ? ` * Run: npm create astro@latest ${n !== "root directory" ? n : "."}` : t === "SvelteKit" ? ` * Run: npm create svelte@latest ${n !== "root directory" ? n : "."}` : t === "Vite" ? ` * Run: npm create vite@latest ${n !== "root directory" ? n : "."} -- --template react-ts` : ` * Run the appropriate CLI command for ${t} to create a new project in ${n !== "root directory" ? n : "current directory"}`}
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: ${n}
143
+ - Location: ${a}
144
144
  - Pages requested: ${i}
145
145
  - Styling: ${o}
146
- - Design approach: ${r}
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
- }, U = () => {
323
- j();
324
- }, D = {
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
- }, F = (e) => D[e.code] ?? `API error: ${e.status} ${e.code}`, M = (e) => {
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
- }, _ = (e) => {
334
- const t = [F(e), e.message, ...M(e)];
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
- }, h = (e) => R(e) ? { content: [{ type: "text", text: _(e) }], isError: !0 } : { content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }], isError: !0 }, $ = () => {
338
- u.tool(
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: a.string().describe(
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: a.string().describe("Display name for the new content entry."),
393
- content: a.array(
394
- a.object({
395
- language: a.string().optional().describe(
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: a.array(
399
- a.object({
400
- name: a.string().describe(
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: a.any().describe(
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 n = g(), o = { contentType: e, name: t, content: i }, r = await n.createContent(o);
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(r, null, 2) }]
417
+ content: [{ type: "text", text: JSON.stringify(s, null, 2) }]
418
418
  };
419
- } catch (n) {
420
- return h(n);
419
+ } catch (a) {
420
+ return p(a);
421
421
  }
422
422
  }
423
423
  );
424
- }, V = () => {
425
- u.tool(
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 g().getProject();
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
- }, m = 25, y = 100, q = () => {
449
- u.tool(
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 ${m}, maximum allowed is ${y}.
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: a.string().optional().describe("Filter by content type name. Must match an existing content type from get-content-island-project."),
475
- id: a.union([a.string(), a.array(a.string())]).optional().describe("A single content id, or an array of ids to fetch a specific subset of contents."),
476
- language: a.string().optional().describe(
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: a.union([a.enum(["draft", "changed", "published"]), a.array(a.enum(["draft", "changed", "published"]))]).optional().describe(
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: a.boolean().optional().describe("When true, related content references are expanded inline in fieldValues."),
483
- sort: a.object({
484
- contentType: a.enum(["asc", "desc"]).optional(),
485
- lastUpdate: a.enum(["asc", "desc"]).optional()
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: a.number().int().min(1).max(y).optional().describe(`Maximum number of items to return. Default ${m}, maximum ${y}.`),
488
- skip: a.number().int().min(0).optional().describe("Number of items to skip from the start of the result set. Use for pagination.")
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: n, includeRelatedContent: o, sort: r, take: s, skip: d }) => {
680
+ async ({ contentType: e, id: t, language: i, status: a, includeRelatedContent: o, sort: s, take: r, skip: d }) => {
491
681
  try {
492
- const l = g(), c = s ?? m, E = d ?? 0, p = {
493
- pagination: { take: c, skip: E }
682
+ const l = h(), c = r ?? T, f = d ?? 0, g = {
683
+ pagination: { take: c, skip: f }
494
684
  };
495
- e !== void 0 && (p.contentType = e), t !== void 0 && (p.id = Array.isArray(t) ? { in: t } : t), i !== void 0 && (p.language = i), n !== void 0 && (p.status = Array.isArray(n) ? { in: n } : n), o !== void 0 && (p.includeRelatedContent = o), r !== void 0 && (p.sort = r);
496
- const I = await l.getRawContentList(p), C = {
497
- items: I,
498
- skip: E,
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: I.length === c
690
+ hasMore: u.length === c
501
691
  };
502
692
  return {
503
- content: [{ type: "text", text: JSON.stringify(C, null, 2) }]
693
+ content: [{ type: "text", text: JSON.stringify(y, null, 2) }]
504
694
  };
505
695
  } catch (l) {
506
- return h(l);
696
+ return p(l);
507
697
  }
508
698
  }
509
699
  );
510
- }, K = (e) => e?.type === "entity", N = (e) => !!(e == null || typeof e == "string" && e === "" || Array.isArray(e) && e.length === 0), b = (e, t) => t && Array.isArray(e) || typeof e == "string" ? e.length : null, W = (e) => {
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
- }, B = (e, t) => {
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 n = W(i);
518
- return n !== null && t.includes(n);
519
- }, H = (e, t, i) => {
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 n = b(t, i.isArray), o = e.customArgs?.length;
526
- return n === null || typeof o != "number" ? null : n < o ? `expected min length ${o}, got ${n}` : null;
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 n = b(t, i.isArray), o = e.customArgs?.length;
530
- return n === null || typeof o != "number" ? null : n > o ? `expected max length ${o}, got ${n}` : null;
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 n = (e.customArgs?.allowedExtensions ?? []).map((s) => s.name);
534
- return n.length === 0 ? null : (i.isArray && Array.isArray(t) ? t : [t]).some((s) => !B(s, n)) ? `media file extension not in allowed list (${n.join(", ")})` : null;
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
- }, Y = (e, t) => {
540
- const i = (e.contentTypes ?? []).find((s) => K(s) && s.name === t.contentType.name);
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 n = [], o = e.languages ?? [], r = t.fields ?? [];
547
- for (const s of i.fields)
548
- if (s.isRequired)
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 = r.find((c) => c.name === s.name && c.language === d);
551
- (!l || N(l.value)) && n.push(`required field "${s.name}" is empty in language "${d}"`);
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 s of r) {
554
- const d = i.fields.find((l) => l.name === s.name);
555
- if (!(!d || N(s.value)))
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 = H(l, s.value, d);
558
- c && n.push(`field "${s.name}" [${s.language}]: ${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 n.length === 0 ? { ok: !0 } : { ok: !1, errors: n };
562
- }, G = (e) => `Cannot publish content — preflight validation failed:
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.`, X = () => {
567
- u.tool(
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: a.string().describe("The id of the existing content entry to publish.")
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 = g(), [i, n] = await Promise.all([t.getProject(), t.getRawContent({ id: e })]);
600
- if (!n)
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 = Y(i, n);
795
+ const o = me(i, a);
606
796
  return o.ok === !1 ? {
607
- content: [{ type: "text", text: G(o.errors) }],
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 h(t);
803
+ return p(t);
614
804
  }
615
805
  }
616
806
  );
617
- }, z = () => {
618
- u.tool(
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: a.string().describe("The id of the existing content entry to update."),
661
- fieldName: a.string().describe("Name of the field as defined in the content type schema (from get-content-island-project)."),
662
- language: a.string().describe('Language code of the field value (e.g. "en", "es"). Must be one of the project languages.'),
663
- value: a.any().describe(
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: n }) => {
857
+ async ({ contentId: e, fieldName: t, language: i, value: a }) => {
668
858
  try {
669
- return await g().updateContentFieldValue(e, { fieldName: t, language: i }, n), {
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 h(o);
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
- }, J = {
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
- }, Q = (e) => /^https?:\/\//i.test(e), Z = async (e) => {
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(), n = new URL(e).pathname, o = A(n) || "download";
1013
+ const i = await t.blob(), a = new URL(e).pathname, o = C(a) || "download";
698
1014
  return { blob: i, fileName: o };
699
- }, ee = async (e) => {
700
- const t = await S(e), i = O(e).toLowerCase(), n = J[i] ?? "application/octet-stream", o = new Blob([t], { type: n }), r = A(e);
701
- return { blob: o, fileName: r };
702
- }, te = () => {
703
- u.tool(
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: a.string().describe(
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: a.string().optional().describe(
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: n } = Q(e) ? await Z(e) : await ee(e), r = await g().uploadMedia({ file: i, fileName: t ?? n });
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(r, null, 2) }]
1034
+ content: [{ type: "text", text: JSON.stringify(s, null, 2) }]
719
1035
  };
720
1036
  } catch (i) {
721
- return h(i);
1037
+ return p(i);
722
1038
  }
723
1039
  }
724
1040
  );
725
- }, ne = () => {
726
- V(), q(), $(), z(), X(), te();
1041
+ }, Ce = () => {
1042
+ se(), le(), B(), fe(), ge(), Ae(), ie(), H(), ve(), ye(), re(), oe();
727
1043
  };
728
- U();
729
- ne();
730
- const ae = new x();
731
- await u.connect(ae);
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.2.2",
3
+ "version": "0.3.1",
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.21.0",
38
+ "@content-island/api-client": "0.23.0",
37
39
  "@modelcontextprotocol/sdk": "1.13.3",
38
40
  "zod": "3.25.71"
39
41
  },