@plank-cms/plank 0.29.0 → 0.30.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.
@@ -12,7 +12,7 @@
12
12
  href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap"
13
13
  rel="stylesheet"
14
14
  />
15
- <script type="module" crossorigin src="/admin/assets/index-2ycjq_gM.js"></script>
15
+ <script type="module" crossorigin src="/admin/assets/index-DAy_fJkl.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/admin/assets/index-CHJ8C--M.css">
17
17
  </head>
18
18
  <body>
package/dist/index.js CHANGED
@@ -93,7 +93,7 @@ function getUpdateScriptCommand(name) {
93
93
  }
94
94
 
95
95
  // src/commands/init.ts
96
- var PACKAGE_VERSION = "0.29.0";
96
+ var PACKAGE_VERSION = "0.30.0";
97
97
  function generateSecret() {
98
98
  return randomBytes(32).toString("hex");
99
99
  }
@@ -203,7 +203,7 @@ import { dirname, join as join3, resolve as resolve2 } from "path";
203
203
  async function start() {
204
204
  config({ path: resolve2(process.cwd(), ".env") });
205
205
  process.env.PLANK_ADMIN_DIST = join3(dirname(fileURLToPath(import.meta.url)), "admin");
206
- const { start: startServer } = await import("./server-KCPJO7V7.js");
206
+ const { start: startServer } = await import("./server-BDQYF2ZA.js");
207
207
  await startServer();
208
208
  }
209
209
 
@@ -4528,7 +4528,7 @@ import { randomBytes as randomBytes7, createHash } from "crypto";
4528
4528
  import { z as z7, flattenError as flattenError5 } from "zod";
4529
4529
  var CreateTokenSchema = z7.object({
4530
4530
  name: z7.string().min(1),
4531
- accessType: z7.enum(["read-only", "full-access"])
4531
+ accessType: z7.enum(["read-only", "full-access", "mcp-server"])
4532
4532
  });
4533
4533
  function hashToken(token) {
4534
4534
  return createHash("sha256").update(token).digest("hex");
@@ -5834,7 +5834,9 @@ import { Router as Router3 } from "express";
5834
5834
  // ../core/dist/middlewares/apiToken.js
5835
5835
  import { createHash as createHash2 } from "crypto";
5836
5836
  var READ_ONLY_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
5837
- async function apiToken(req, res, next) {
5837
+ var PUBLIC_API_ACCESS_TYPES = /* @__PURE__ */ new Set(["read-only", "full-access"]);
5838
+ var MCP_ACCESS_TYPES = /* @__PURE__ */ new Set(["mcp-server"]);
5839
+ async function enforceApiToken(req, res, next, allowedAccessTypes) {
5838
5840
  const header = req.headers.authorization;
5839
5841
  if (!header?.startsWith("Bearer ")) {
5840
5842
  res.status(401).json({ error: "API token required" });
@@ -5847,12 +5849,26 @@ async function apiToken(req, res, next) {
5847
5849
  res.status(401).json({ error: "Invalid API token" });
5848
5850
  return;
5849
5851
  }
5852
+ if (!allowedAccessTypes.has(rows[0].access_type)) {
5853
+ res.status(403).json({ error: "This token cannot access this endpoint" });
5854
+ return;
5855
+ }
5850
5856
  if (rows[0].access_type === "read-only" && !READ_ONLY_METHODS.has(req.method)) {
5851
5857
  res.status(403).json({ error: "This token only allows read access" });
5852
5858
  return;
5853
5859
  }
5860
+ req.apiToken = {
5861
+ id: rows[0].id,
5862
+ accessType: rows[0].access_type
5863
+ };
5854
5864
  next();
5855
5865
  }
5866
+ async function apiToken(req, res, next) {
5867
+ await enforceApiToken(req, res, next, PUBLIC_API_ACCESS_TYPES);
5868
+ }
5869
+ async function mcpToken(req, res, next) {
5870
+ await enforceApiToken(req, res, next, MCP_ACCESS_TYPES);
5871
+ }
5856
5872
 
5857
5873
  // ../core/dist/controllers/public.js
5858
5874
  function createMediaValue(url, options) {
@@ -6453,6 +6469,276 @@ router3.get("/:slug", listPublicEntries);
6453
6469
  router3.get("/:slug/:id", getPublicEntry);
6454
6470
  var public_default = router3;
6455
6471
 
6472
+ // ../core/dist/routes/mcp.js
6473
+ import { Router as Router4 } from "express";
6474
+
6475
+ // ../core/dist/controllers/mcp.js
6476
+ var MCP_PROTOCOL_VERSION = "2025-11-25";
6477
+ var JSON_RPC_VERSION = "2.0";
6478
+ var CONTENT_TYPES_URI = "plank://content-types";
6479
+ var LOCALES_URI = "plank://locales";
6480
+ function buildSchemaUri(slug) {
6481
+ return `plank://content-types/${slug}/schema`;
6482
+ }
6483
+ function parseLocales(raw, fallback) {
6484
+ if (!raw)
6485
+ return [fallback];
6486
+ try {
6487
+ const parsed = JSON.parse(raw);
6488
+ if (!Array.isArray(parsed))
6489
+ return [fallback];
6490
+ const locales = parsed.filter((value) => typeof value === "string" && value.length > 0);
6491
+ return locales.length > 0 ? [...new Set(locales)] : [fallback];
6492
+ } catch {
6493
+ return [fallback];
6494
+ }
6495
+ }
6496
+ async function getLocalesPayload() {
6497
+ const settings = await getSettings("general");
6498
+ const defaultLocale = settings.default_locale ?? "en";
6499
+ const locales = parseLocales(settings.locales, defaultLocale);
6500
+ if (!locales.includes(defaultLocale)) {
6501
+ locales.unshift(defaultLocale);
6502
+ }
6503
+ return { defaultLocale, locales };
6504
+ }
6505
+ async function getContentTypeSummaries() {
6506
+ const contentTypes = await findAllContentTypes();
6507
+ return contentTypes.map((contentType) => ({
6508
+ name: contentType.name,
6509
+ slug: contentType.slug,
6510
+ kind: contentType.kind,
6511
+ previewEnabled: contentType.previewEnabled ?? true,
6512
+ isDefault: contentType.isDefault ?? false,
6513
+ schemaUri: buildSchemaUri(contentType.slug),
6514
+ updatedAt: contentType.updatedAt?.toISOString() ?? null
6515
+ }));
6516
+ }
6517
+ async function listResources() {
6518
+ const contentTypes = await getContentTypeSummaries();
6519
+ return [
6520
+ {
6521
+ name: "content-types",
6522
+ title: "Content Types",
6523
+ uri: CONTENT_TYPES_URI,
6524
+ description: "Lists the content types available in this Plank instance.",
6525
+ mimeType: "application/json"
6526
+ },
6527
+ {
6528
+ name: "locales",
6529
+ title: "Locales",
6530
+ uri: LOCALES_URI,
6531
+ description: "Lists the enabled locales and the default locale for this Plank instance.",
6532
+ mimeType: "application/json"
6533
+ },
6534
+ ...contentTypes.map((contentType) => ({
6535
+ name: `content-type-schema-${contentType.slug}`,
6536
+ title: `${contentType.name} schema`,
6537
+ uri: contentType.schemaUri,
6538
+ description: `Schema for the "${contentType.slug}" content type.`,
6539
+ mimeType: "application/json",
6540
+ annotations: contentType.updatedAt ? {
6541
+ lastModified: contentType.updatedAt
6542
+ } : void 0
6543
+ }))
6544
+ ];
6545
+ }
6546
+ async function readResource(uri) {
6547
+ if (uri === CONTENT_TYPES_URI) {
6548
+ return {
6549
+ contents: [
6550
+ {
6551
+ uri,
6552
+ mimeType: "application/json",
6553
+ text: JSON.stringify({ contentTypes: await getContentTypeSummaries() }, null, 2)
6554
+ }
6555
+ ]
6556
+ };
6557
+ }
6558
+ if (uri === LOCALES_URI) {
6559
+ return {
6560
+ contents: [
6561
+ {
6562
+ uri,
6563
+ mimeType: "application/json",
6564
+ text: JSON.stringify(await getLocalesPayload(), null, 2)
6565
+ }
6566
+ ]
6567
+ };
6568
+ }
6569
+ const match = /^plank:\/\/content-types\/([^/]+)\/schema$/.exec(uri);
6570
+ if (!match) {
6571
+ throw buildJsonRpcError(-32602, "Unknown resource URI", { uri });
6572
+ }
6573
+ const slug = decodeURIComponent(match[1] ?? "");
6574
+ const contentType = await findContentTypeBySlug(slug);
6575
+ if (!contentType) {
6576
+ throw buildJsonRpcError(-32004, "Content type not found", { slug });
6577
+ }
6578
+ return {
6579
+ contents: [
6580
+ {
6581
+ uri,
6582
+ mimeType: "application/json",
6583
+ text: JSON.stringify(contentType, null, 2)
6584
+ }
6585
+ ]
6586
+ };
6587
+ }
6588
+ function buildJsonRpcError(code, message, data) {
6589
+ return data === void 0 ? { code, message } : { code, message, data };
6590
+ }
6591
+ function buildResult(id, result) {
6592
+ return {
6593
+ jsonrpc: JSON_RPC_VERSION,
6594
+ id,
6595
+ result
6596
+ };
6597
+ }
6598
+ function buildError(id, error) {
6599
+ return {
6600
+ jsonrpc: JSON_RPC_VERSION,
6601
+ id,
6602
+ error
6603
+ };
6604
+ }
6605
+ function isJsonRpcRequest(value) {
6606
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6607
+ }
6608
+ async function handleRequest(message) {
6609
+ if (message.jsonrpc !== JSON_RPC_VERSION) {
6610
+ return buildError(message.id ?? null, buildJsonRpcError(-32600, "Invalid JSON-RPC version"));
6611
+ }
6612
+ if (typeof message.method !== "string" || message.method.length === 0) {
6613
+ return buildError(message.id ?? null, buildJsonRpcError(-32600, "Invalid method"));
6614
+ }
6615
+ if (message.id === void 0) {
6616
+ if (message.method === "notifications/initialized") {
6617
+ return null;
6618
+ }
6619
+ return null;
6620
+ }
6621
+ try {
6622
+ switch (message.method) {
6623
+ case "initialize": {
6624
+ const version = await getCurrentVersion();
6625
+ return buildResult(message.id, {
6626
+ protocolVersion: MCP_PROTOCOL_VERSION,
6627
+ capabilities: {
6628
+ resources: {}
6629
+ },
6630
+ serverInfo: {
6631
+ name: "plank-cms",
6632
+ title: "Plank CMS",
6633
+ version
6634
+ },
6635
+ instructions: "Use resources/list to discover available resources, then resources/read to load Plank content types, per-type schemas, and locales."
6636
+ });
6637
+ }
6638
+ case "ping":
6639
+ return buildResult(message.id, {});
6640
+ case "resources/list":
6641
+ return buildResult(message.id, { resources: await listResources() });
6642
+ case "resources/read": {
6643
+ const uri = message.params?.uri;
6644
+ if (typeof uri !== "string" || uri.length === 0) {
6645
+ return buildError(message.id, buildJsonRpcError(-32602, "A resource URI is required"));
6646
+ }
6647
+ return buildResult(message.id, await readResource(uri));
6648
+ }
6649
+ default:
6650
+ return buildError(message.id, buildJsonRpcError(-32601, "Method not found", { method: message.method }));
6651
+ }
6652
+ } catch (error) {
6653
+ if (isJsonRpcError(error)) {
6654
+ return buildError(message.id, error);
6655
+ }
6656
+ return buildError(message.id, buildJsonRpcError(-32603, "Internal server error"));
6657
+ }
6658
+ }
6659
+ function isJsonRpcError(error) {
6660
+ return typeof error === "object" && error !== null && "code" in error && "message" in error;
6661
+ }
6662
+ async function handleMcpRequest(req, res) {
6663
+ const payload = req.body;
6664
+ if (Array.isArray(payload)) {
6665
+ if (payload.length === 0) {
6666
+ res.status(400).json(buildError(null, buildJsonRpcError(-32600, "Batch requests cannot be empty")));
6667
+ return;
6668
+ }
6669
+ const responses = (await Promise.all(payload.map(async (message) => {
6670
+ if (!isJsonRpcRequest(message)) {
6671
+ return buildError(null, buildJsonRpcError(-32600, "Invalid request"));
6672
+ }
6673
+ return await handleRequest(message);
6674
+ }))).filter((response2) => response2 !== null);
6675
+ if (responses.length === 0) {
6676
+ res.status(202).end();
6677
+ return;
6678
+ }
6679
+ res.json(responses);
6680
+ return;
6681
+ }
6682
+ if (!isJsonRpcRequest(payload)) {
6683
+ res.status(400).json(buildError(null, buildJsonRpcError(-32600, "Invalid request")));
6684
+ return;
6685
+ }
6686
+ const response = await handleRequest(payload);
6687
+ if (!response) {
6688
+ res.status(202).end();
6689
+ return;
6690
+ }
6691
+ res.json(response);
6692
+ }
6693
+ function handleMcpGet(_req, res) {
6694
+ res.status(200).json({
6695
+ name: "plank-cms",
6696
+ transport: "streamable-http",
6697
+ endpoint: "/mcp",
6698
+ protocolVersion: MCP_PROTOCOL_VERSION,
6699
+ capabilities: ["resources"]
6700
+ });
6701
+ }
6702
+ function handleMcpDelete(_req, res) {
6703
+ res.status(405).json({ error: "Session termination is not supported" });
6704
+ }
6705
+
6706
+ // ../core/dist/middlewares/mcpOrigin.js
6707
+ function validateMcpOrigin(req, res, next) {
6708
+ const origin = req.get("origin");
6709
+ if (!origin) {
6710
+ next();
6711
+ return;
6712
+ }
6713
+ const host = req.get("host");
6714
+ if (!host) {
6715
+ res.status(400).json({ error: "Missing Host header" });
6716
+ return;
6717
+ }
6718
+ const forwardedProto = req.get("x-forwarded-proto");
6719
+ const protocol = forwardedProto ? forwardedProto.split(",")[0].trim() : req.protocol;
6720
+ const allowedOrigins = /* @__PURE__ */ new Set([`${protocol}://${host}`]);
6721
+ if (!process.env.PLANK_ADMIN_DIST) {
6722
+ const port = process.env.PLANK_PORT ?? "5500";
6723
+ allowedOrigins.add(`http://localhost:${port}`);
6724
+ allowedOrigins.add("http://localhost:3000");
6725
+ }
6726
+ if (!allowedOrigins.has(origin)) {
6727
+ res.status(403).json({ error: "Origin not allowed" });
6728
+ return;
6729
+ }
6730
+ next();
6731
+ }
6732
+
6733
+ // ../core/dist/routes/mcp.js
6734
+ var router4 = Router4();
6735
+ router4.use(validateMcpOrigin);
6736
+ router4.use(mcpToken);
6737
+ router4.get("/", handleMcpGet);
6738
+ router4.post("/", handleMcpRequest);
6739
+ router4.delete("/", handleMcpDelete);
6740
+ var mcp_default = router4;
6741
+
6456
6742
  // ../core/dist/middlewares/errorHandler.js
6457
6743
  import { ZodError, flattenError as flattenError6 } from "zod";
6458
6744
  function parseUniqueViolationField(detail) {
@@ -6523,6 +6809,7 @@ var cmsCorOptions = cors({ origin: cmsAllowedOrigins, credentials: true });
6523
6809
  app.use("/cms/auth", cmsCorOptions, auth_default);
6524
6810
  app.use("/cms/admin", cmsCorOptions, admin_default);
6525
6811
  app.use("/api", cors(), public_default);
6812
+ app.use("/mcp", mcp_default);
6526
6813
  app.get("/", (_req, res) => {
6527
6814
  if (isDev) {
6528
6815
  res.redirect(adminDevUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plank-cms/plank",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Self-hosted headless CMS. Deploy in minutes on your own infrastructure.",
5
5
  "type": "module",
6
6
  "files": [
@@ -55,9 +55,9 @@
55
55
  "devDependencies": {
56
56
  "@types/fs-extra": "^11.0.4",
57
57
  "tsup": "^8.5.0",
58
- "@plank-cms/core": "0.29.0",
59
- "@plank-cms/schema": "0.29.0",
60
- "@plank-cms/db": "0.29.0"
58
+ "@plank-cms/core": "0.30.0",
59
+ "@plank-cms/db": "0.30.0",
60
+ "@plank-cms/schema": "0.30.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",