@paroicms/mcp-plugin 0.2.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 ADDED
@@ -0,0 +1,58 @@
1
+ # ParoiCMS MCP Plugin (`@paroicms/mcp-plugin`)
2
+
3
+ MCP (Model Context Protocol) plugin for ParoiCMS. Enables AI assistants to search content in ParoiCMS sites. It contains:
4
+
5
+ 1. A backend plugin that exposes an HTTP API protected by Personal Access Tokens.
6
+ 2. A CLI tool that acts as an MCP server bridge between the AI agent and the remote backend.
7
+
8
+ This package is part of [ParoiCMS](https://www.npmjs.com/package/@paroicms/server).
9
+
10
+ ## Installation of the backend plugin
11
+
12
+ Install it:
13
+
14
+ ```sh
15
+ npm i @paroicms/mcp-plugin
16
+ ```
17
+
18
+ Enable it in your `site-schema.json`:
19
+
20
+ ```json
21
+ {
22
+ "plugins": [
23
+ // ...
24
+ "@paroicms/mcp-plugin"
25
+ ]
26
+ }
27
+ ```
28
+
29
+ ## Configuration of the MCP agent with the CLI tool
30
+
31
+ ### 1. Create a Personal Access Token
32
+
33
+ 1. Go to your ParoiCMS Admin UI
34
+ 2. Navigate to User Settings → Personal Access Tokens
35
+ 3. Copy the token (it will only be shown once)
36
+
37
+ ### 2. Configure your agent
38
+
39
+ Add to your configuration in `.vscode/mcp.json` (VS Code):
40
+
41
+ ```json
42
+ {
43
+ "servers": {
44
+ "paroicms-yoursite": {
45
+ "command": "npx",
46
+ "args": ["paroicms-mcp"],
47
+ "env": {
48
+ "PAROICMS_SITE_URL": "https://your-site.example.com",
49
+ "PAROICMS_PAT": "your-personal-access-token"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## License
57
+
58
+ Released under the [MIT license](https://gitlab.com/paroi/opensource/paroicms/-/blob/main/LICENSE.md).
@@ -0,0 +1,76 @@
1
+ import { messageOf } from "@paroicms/public-anywhere-lib";
2
+ import { ApiError, } from "@paroicms/public-server-lib";
3
+ import { McpApiRequestAT } from "./api-request-types.js";
4
+ export async function handleApiRequest(service, httpContext, relativePath) {
5
+ const { req, res } = httpContext;
6
+ if (relativePath !== "")
7
+ throw new ApiError(404);
8
+ if (req.method !== "POST")
9
+ throw new ApiError(405);
10
+ const pat = extractBearerToken(req);
11
+ try {
12
+ const request = McpApiRequestAT.assert(req.body);
13
+ const connector = service.getSiteConnector({ pat });
14
+ const result = await executeAction(connector, request);
15
+ res.send({ success: true, data: result });
16
+ }
17
+ catch (error) {
18
+ if (error instanceof ApiError) {
19
+ res.status(error.status).send({
20
+ success: false,
21
+ error: error.message,
22
+ code: error.status,
23
+ });
24
+ }
25
+ else {
26
+ service.logger.error("MCP API error:", error);
27
+ res.status(500).send({
28
+ success: false,
29
+ error: messageOf(error),
30
+ });
31
+ }
32
+ }
33
+ }
34
+ async function executeAction(connector, request) {
35
+ switch (request.action) {
36
+ case "searchDocuments":
37
+ return await handleSearchDocuments(connector, request.payload);
38
+ case "deleteDocument":
39
+ await connector.deleteDocument(request.payload.documentId);
40
+ return { success: true };
41
+ case "publishDocument":
42
+ await connector.publishDocument(request.payload.documentId, request.payload.publishDate);
43
+ return { success: true };
44
+ case "unpublishDocument":
45
+ await connector.unpublishDocument(request.payload.documentId);
46
+ return { success: true };
47
+ case "getDocument":
48
+ return await connector.getDocument(request.payload.documentId);
49
+ case "updateDocument":
50
+ await connector.updateDocument(request.payload.documentId, request.payload.values);
51
+ return { success: true };
52
+ case "updateFields":
53
+ await connector.updateFields(request.payload.lNodeId, request.payload.values);
54
+ return { success: true };
55
+ case "createDocument": {
56
+ const result = await connector.createDocument(request.payload);
57
+ return { lNodeId: result.documentId };
58
+ }
59
+ case "createPart": {
60
+ const result = await connector.createPart(request.payload);
61
+ return { lNodeId: result.partId };
62
+ }
63
+ case "getSiteInfo":
64
+ return await connector.getSiteInfo();
65
+ }
66
+ }
67
+ async function handleSearchDocuments(connector, payload) {
68
+ return await connector.searchDocuments(payload);
69
+ }
70
+ function extractBearerToken(req) {
71
+ const authHeader = req.headers.authorization;
72
+ if (!authHeader?.startsWith("Bearer ")) {
73
+ throw new ApiError("Missing or invalid Authorization header", 401);
74
+ }
75
+ return authHeader.slice(7);
76
+ }
@@ -0,0 +1,105 @@
1
+ import { type } from "arktype";
2
+ const SearchDocumentsMcpApiRequestAT = type({
3
+ action: '"searchDocuments"',
4
+ payload: {
5
+ language: "string",
6
+ words: "string[]",
7
+ "limit?": "number|undefined",
8
+ "offset?": "number|undefined",
9
+ "+": "reject",
10
+ },
11
+ "+": "reject",
12
+ });
13
+ const DeleteDocumentMcpApiRequestAT = type({
14
+ action: '"deleteDocument"',
15
+ payload: {
16
+ documentId: "string",
17
+ "+": "reject",
18
+ },
19
+ "+": "reject",
20
+ });
21
+ const PublishDocumentMcpApiRequestAT = type({
22
+ action: '"publishDocument"',
23
+ payload: {
24
+ documentId: "string",
25
+ "publishDate?": "string|undefined",
26
+ "+": "reject",
27
+ },
28
+ "+": "reject",
29
+ });
30
+ const UnpublishDocumentMcpApiRequestAT = type({
31
+ action: '"unpublishDocument"',
32
+ payload: {
33
+ documentId: "string",
34
+ "+": "reject",
35
+ },
36
+ "+": "reject",
37
+ });
38
+ const GetDocumentMcpApiRequestAT = type({
39
+ action: '"getDocument"',
40
+ payload: {
41
+ documentId: "string",
42
+ "+": "reject",
43
+ },
44
+ "+": "reject",
45
+ });
46
+ const UpdateDocumentMcpApiRequestAT = type({
47
+ action: '"updateDocument"',
48
+ payload: {
49
+ documentId: "string",
50
+ values: {
51
+ "title?": "string|undefined",
52
+ "slug?": "string|undefined",
53
+ "metaDescription?": "string|undefined",
54
+ "metaKeywords?": "string|undefined",
55
+ "+": "reject",
56
+ },
57
+ "+": "reject",
58
+ },
59
+ "+": "reject",
60
+ });
61
+ const UpdateFieldsMcpApiRequestAT = type({
62
+ action: '"updateFields"',
63
+ payload: {
64
+ lNodeId: "string",
65
+ values: "object",
66
+ "+": "reject",
67
+ },
68
+ "+": "reject",
69
+ });
70
+ const CreateDocumentMcpApiRequestAT = type({
71
+ action: '"createDocument"',
72
+ payload: {
73
+ parentLNodeId: "string",
74
+ typeName: "string",
75
+ "title?": "string|undefined",
76
+ "slug?": "string|undefined",
77
+ "featuredImagePath?": "string|undefined",
78
+ "values?": "object|undefined",
79
+ "+": "reject",
80
+ },
81
+ "+": "reject",
82
+ });
83
+ const CreatePartMcpApiRequestAT = type({
84
+ action: '"createPart"',
85
+ payload: {
86
+ parentLNodeId: "string",
87
+ typeName: "string",
88
+ "values?": "object|undefined",
89
+ "+": "reject",
90
+ },
91
+ "+": "reject",
92
+ });
93
+ const GetSiteInfoMcpApiRequestAT = type({
94
+ action: '"getSiteInfo"',
95
+ "+": "reject",
96
+ });
97
+ export const McpApiRequestAT = SearchDocumentsMcpApiRequestAT.or(DeleteDocumentMcpApiRequestAT)
98
+ .or(PublishDocumentMcpApiRequestAT)
99
+ .or(UnpublishDocumentMcpApiRequestAT)
100
+ .or(GetDocumentMcpApiRequestAT)
101
+ .or(UpdateDocumentMcpApiRequestAT)
102
+ .or(UpdateFieldsMcpApiRequestAT)
103
+ .or(CreateDocumentMcpApiRequestAT)
104
+ .or(CreatePartMcpApiRequestAT)
105
+ .or(GetSiteInfoMcpApiRequestAT);
@@ -0,0 +1,13 @@
1
+ import { esmDirName, extractPackageNameAndVersionSync } from "@paroicms/script-lib";
2
+ import { dirname } from "node:path";
3
+ import { handleApiRequest } from "./api-handler.js";
4
+ const projectDir = dirname(esmDirName(import.meta.url));
5
+ const packageDir = dirname(projectDir);
6
+ const { version } = extractPackageNameAndVersionSync(packageDir);
7
+ const plugin = {
8
+ version,
9
+ siteInit(service) {
10
+ service.setPublicApiHandler(handleApiRequest);
11
+ },
12
+ };
13
+ export default plugin;
@@ -0,0 +1,3 @@
1
+ import { esmDirName } from "@paroicms/script-lib";
2
+ import { dirname } from "node:path";
3
+ export const packageDir = dirname(dirname(esmDirName(import.meta.url)));
@@ -0,0 +1,21 @@
1
+ import { getPluginApiUrl } from "@paroicms/script-lib";
2
+ const PLUGIN_NAME = "@paroicms/mcp-plugin";
3
+ export async function callApi(config, action, payload) {
4
+ const url = `${config.siteUrl}${getPluginApiUrl(PLUGIN_NAME)}`;
5
+ const response = await fetch(url, {
6
+ method: "POST",
7
+ headers: {
8
+ "Content-Type": "application/json",
9
+ Authorization: `Bearer ${config.pat}`,
10
+ },
11
+ body: JSON.stringify({ action, payload }),
12
+ });
13
+ if (!response.ok) {
14
+ throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
15
+ }
16
+ const data = await response.json();
17
+ if (!data.success) {
18
+ throw new Error(data.error ?? "Unknown API error");
19
+ }
20
+ return data.data;
21
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { startMcpServer } from "./mcp-server.js";
3
+ startMcpServer().catch((error) => {
4
+ console.error("Failed to start MCP server:", error);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,20 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { registerTools } from "./tools/index.js";
4
+ export async function startMcpServer() {
5
+ const siteUrl = process.env.PAROICMS_SITE_URL;
6
+ const pat = process.env.PAROICMS_PAT;
7
+ if (siteUrl === undefined) {
8
+ throw new Error("PAROICMS_SITE_URL environment variable is required");
9
+ }
10
+ if (pat === undefined) {
11
+ throw new Error("PAROICMS_PAT environment variable is required");
12
+ }
13
+ const server = new McpServer({
14
+ name: "paroicms-mcp",
15
+ version: "0.1.0",
16
+ });
17
+ registerTools(server, { siteUrl, pat });
18
+ const transport = new StdioServerTransport();
19
+ await server.connect(transport);
20
+ }
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerCreateDocumentTool(server, config) {
4
+ server.registerTool("paroicms_create_document", {
5
+ description: "Create a new document in ParoiCMS",
6
+ inputSchema: {
7
+ parentLNodeId: z
8
+ .string()
9
+ .describe("The parent's localized node ID (format: nodeId:language, e.g., '123:en')"),
10
+ typeName: z.string().describe("The document type name (e.g., 'article', 'blogPost')"),
11
+ title: z.string().optional().describe("Document title"),
12
+ slug: z
13
+ .string()
14
+ .optional()
15
+ .describe("URL slug (auto-generated from title if not provided)"),
16
+ values: z.string().optional().describe("Optional JSON object with field values"),
17
+ },
18
+ }, async ({ parentLNodeId, typeName, title, slug, values: valuesJson }) => {
19
+ let values;
20
+ if (valuesJson) {
21
+ try {
22
+ values = JSON.parse(valuesJson);
23
+ }
24
+ catch {
25
+ return {
26
+ content: [{ type: "text", text: "Invalid JSON for values parameter." }],
27
+ isError: true,
28
+ };
29
+ }
30
+ }
31
+ const result = await callApi(config, "createDocument", {
32
+ parentLNodeId,
33
+ typeName,
34
+ title,
35
+ slug,
36
+ values,
37
+ });
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: `Document created successfully.\nID: ${result.lNodeId}`,
43
+ },
44
+ ],
45
+ };
46
+ });
47
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerCreatePartTool(server, config) {
4
+ server.registerTool("paroicms_create_part", {
5
+ description: "Create a new part (sub-section) within a document or another part in ParoiCMS",
6
+ inputSchema: {
7
+ parentLNodeId: z
8
+ .string()
9
+ .describe("The parent's localized node ID (format: nodeId:language, e.g., '123:en')"),
10
+ typeName: z.string().describe("The part type name (e.g., 'textBlock', 'imageGallery')"),
11
+ values: z.string().optional().describe("Optional JSON object with field values"),
12
+ },
13
+ }, async ({ parentLNodeId, typeName, values: valuesJson }) => {
14
+ let values;
15
+ if (valuesJson) {
16
+ try {
17
+ values = JSON.parse(valuesJson);
18
+ }
19
+ catch {
20
+ return {
21
+ content: [{ type: "text", text: "Invalid JSON for values parameter." }],
22
+ isError: true,
23
+ };
24
+ }
25
+ }
26
+ const result = await callApi(config, "createPart", {
27
+ parentLNodeId,
28
+ typeName,
29
+ values,
30
+ });
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text",
35
+ text: `Part created successfully.\nID: ${result.lNodeId}`,
36
+ },
37
+ ],
38
+ };
39
+ });
40
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerDeleteDocumentTool(server, config) {
4
+ server.registerTool("paroicms_delete_document", {
5
+ description: "Delete a document from ParoiCMS (single language version)",
6
+ inputSchema: {
7
+ documentId: z.string().describe("The document ID"),
8
+ },
9
+ }, async ({ documentId }) => {
10
+ await callApi(config, "deleteDocument", { documentId });
11
+ return {
12
+ content: [{ type: "text", text: `Document "${documentId}" deleted successfully.` }],
13
+ };
14
+ });
15
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerGetDocumentTool(server, config) {
4
+ server.registerTool("paroicms_get_document", {
5
+ description: "Get a document from ParoiCMS with all its field values and parts",
6
+ inputSchema: {
7
+ documentId: z.string().describe("The document ID"),
8
+ },
9
+ }, async ({ documentId }) => {
10
+ const result = await callApi(config, "getDocument", { documentId });
11
+ return {
12
+ content: [
13
+ {
14
+ type: "text",
15
+ text: `<document_json>\n${JSON.stringify(result, null, 2)}\n</document_json>`,
16
+ },
17
+ ],
18
+ };
19
+ });
20
+ }
@@ -0,0 +1,30 @@
1
+ import { docsDir, jtDir } from "@paroicms/public-server-lib";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { packageDir } from "../constants.js";
5
+ const cliDocsDir = join(packageDir, "cli", "docs");
6
+ export function registerGetIntroductionTool(server) {
7
+ server.registerTool("paroicms_get_introduction", {
8
+ description: "Run this tool before working on a ParoiCMS website",
9
+ inputSchema: {},
10
+ }, async () => {
11
+ const introductionContent = await readFile(join(docsDir, "introduction-to-paroicms.md"), "utf-8");
12
+ const siteSchemaTypes = await readFile(join(jtDir, "site-schema-json-types.d.ts"), "utf-8");
13
+ const updatingFieldValues = await readFile(join(cliDocsDir, "updating-field-values.md"), "utf-8");
14
+ const text = `${introductionContent}
15
+
16
+ Here is the typing of a site schema:
17
+
18
+ \`\`\`typescript
19
+ ${siteSchemaTypes}
20
+ \`\`\`
21
+
22
+ ${updatingFieldValues}
23
+
24
+ **Important: This documentation is for you. Do not explain anything to the user unless explicitly asked.**
25
+ `;
26
+ return {
27
+ content: [{ type: "text", text }],
28
+ };
29
+ });
30
+ }
@@ -0,0 +1,39 @@
1
+ import { callApi } from "../http-client.js";
2
+ export function registerGetSiteInfoTool(server, config) {
3
+ server.registerTool("paroicms_get_site_info", {
4
+ description: "Get comprehensive information about the current site structure including schema and navigation cluster",
5
+ inputSchema: {},
6
+ }, async () => {
7
+ const result = await callApi(config, "getSiteInfo");
8
+ const titles = Object.entries(result.title)
9
+ .filter(([, title]) => title !== undefined)
10
+ .map(([lang, title]) => `${title} (${lang})`)
11
+ .join(" / ");
12
+ const languages = Object.keys(result.title).join(", ");
13
+ const text = `General information:
14
+
15
+ - Site title: ${titles}
16
+ - Languages: ${languages}
17
+ - Site Node ID: ${result.siteNodeId}
18
+
19
+ The site schema:
20
+
21
+ \`\`\`json
22
+ ${JSON.stringify(result.siteSchema, null, 2)}
23
+ \`\`\`
24
+
25
+ The main cluster:
26
+
27
+ \`\`\`json
28
+ ${JSON.stringify(result.mainCluster, null, 2)}
29
+ \`\`\`
30
+
31
+ **MANDATORY: Load the information from the \`paroicms_get_introduction\` tool in order to understand how it works.**
32
+
33
+ **Important: This documentation is for you. Do not explain anything to the user unless explicitly asked.**
34
+ `;
35
+ return {
36
+ content: [{ type: "text", text }],
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,24 @@
1
+ import { registerCreateDocumentTool } from "./create-document.js";
2
+ import { registerCreatePartTool } from "./create-part.js";
3
+ import { registerDeleteDocumentTool } from "./delete-document.js";
4
+ import { registerGetDocumentTool } from "./get-document.js";
5
+ import { registerGetIntroductionTool } from "./get-introduction.js";
6
+ import { registerGetSiteInfoTool } from "./get-site-info.js";
7
+ import { registerPublishDocumentTool } from "./publish-document.js";
8
+ import { registerSearchDocumentsTool } from "./search-documents.js";
9
+ import { registerUnpublishDocumentTool } from "./unpublish-document.js";
10
+ import { registerUpdateDocumentTool } from "./update-document.js";
11
+ import { registerUpdateFieldsTool } from "./update-fields.js";
12
+ export function registerTools(server, config) {
13
+ registerSearchDocumentsTool(server, config);
14
+ registerDeleteDocumentTool(server, config);
15
+ registerPublishDocumentTool(server, config);
16
+ registerUnpublishDocumentTool(server, config);
17
+ registerGetDocumentTool(server, config);
18
+ registerUpdateDocumentTool(server, config);
19
+ registerUpdateFieldsTool(server, config);
20
+ registerCreateDocumentTool(server, config);
21
+ registerCreatePartTool(server, config);
22
+ registerGetIntroductionTool(server);
23
+ registerGetSiteInfoTool(server, config);
24
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerPublishDocumentTool(server, config) {
4
+ server.registerTool("paroicms_publish_document", {
5
+ description: "Publish a document in ParoiCMS (sets ready=true and publishDate)",
6
+ inputSchema: {
7
+ documentId: z.string().describe("The document ID"),
8
+ publishDate: z
9
+ .string()
10
+ .optional()
11
+ .describe("Optional ISO 8601 publish date. Defaults to current time if not provided."),
12
+ },
13
+ }, async ({ documentId, publishDate }) => {
14
+ await callApi(config, "publishDocument", { documentId, publishDate });
15
+ return {
16
+ content: [{ type: "text", text: `Document "${documentId}" published successfully.` }],
17
+ };
18
+ });
19
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerSearchDocumentsTool(server, config) {
4
+ server.registerTool("paroicms_search_documents", {
5
+ description: "Search for documents in ParoiCMS by keywords",
6
+ inputSchema: {
7
+ language: z.string().describe("The language code (e.g., 'en', 'fr')"),
8
+ keywords: z.string().describe("Space-separated keywords to search for"),
9
+ limit: z.number().optional().describe("Maximum number of results (default: 20)"),
10
+ offset: z.number().optional().describe("Number of results to skip (for pagination)"),
11
+ },
12
+ }, async ({ language, keywords, limit, offset }) => {
13
+ const words = keywords
14
+ .split(/\s+/)
15
+ .map((w) => w.trim())
16
+ .filter((w) => w.length > 0);
17
+ if (words.length === 0) {
18
+ return {
19
+ content: [{ type: "text", text: "Error: No valid keywords provided" }],
20
+ isError: true,
21
+ };
22
+ }
23
+ const result = await callApi(config, "searchDocuments", {
24
+ language,
25
+ words,
26
+ limit,
27
+ offset,
28
+ });
29
+ const count = result.total ?? result.items.length;
30
+ const responseText = result.items.length > 0
31
+ ? `Found ${count} document${count !== 1 ? "s" : ""}:\n\n<documents_json>\n${JSON.stringify(result.items, null, 2)}\n</documents_json>`
32
+ : "No documents found matching your search.";
33
+ return {
34
+ content: [{ type: "text", text: responseText }],
35
+ };
36
+ });
37
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerUnpublishDocumentTool(server, config) {
4
+ server.registerTool("paroicms_unpublish_document", {
5
+ description: "Unpublish a document in ParoiCMS (sets ready=false, keeps publishDate)",
6
+ inputSchema: {
7
+ documentId: z.string().describe("The document ID"),
8
+ },
9
+ }, async ({ documentId }) => {
10
+ await callApi(config, "unpublishDocument", { documentId });
11
+ return {
12
+ content: [{ type: "text", text: `Document "${documentId}" unpublished successfully.` }],
13
+ };
14
+ });
15
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerUpdateDocumentTool(server, config) {
4
+ server.registerTool("paroicms_update_document", {
5
+ description: "Update document metadata (title, slug, metaDescription, metaKeywords) in ParoiCMS",
6
+ inputSchema: {
7
+ documentId: z.string().describe("The document ID"),
8
+ title: z.string().optional().describe("New title for the document"),
9
+ slug: z.string().optional().describe("New slug for the document URL"),
10
+ metaDescription: z.string().optional().describe("Meta description for SEO"),
11
+ metaKeywords: z.string().optional().describe("Meta keywords for SEO"),
12
+ },
13
+ }, async ({ documentId, title, slug, metaDescription, metaKeywords }) => {
14
+ const values = {};
15
+ if (title !== undefined)
16
+ values.title = title;
17
+ if (slug !== undefined)
18
+ values.slug = slug;
19
+ if (metaDescription !== undefined)
20
+ values.metaDescription = metaDescription;
21
+ if (metaKeywords !== undefined)
22
+ values.metaKeywords = metaKeywords;
23
+ if (Object.keys(values).length === 0) {
24
+ return {
25
+ content: [{ type: "text", text: "No values provided to update." }],
26
+ isError: true,
27
+ };
28
+ }
29
+ await callApi(config, "updateDocument", { documentId, values });
30
+ return {
31
+ content: [{ type: "text", text: `Document "${documentId}" updated successfully.` }],
32
+ };
33
+ });
34
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { callApi } from "../http-client.js";
3
+ export function registerUpdateFieldsTool(server, config) {
4
+ server.registerTool("paroicms_update_fields", {
5
+ description: "Update field values on a document, part, or site node in ParoiCMS. Use null to clear a field.",
6
+ inputSchema: {
7
+ lNodeId: z
8
+ .string()
9
+ .describe("The localized node ID (format: nodeId:language, e.g., '123:en')"),
10
+ values: z
11
+ .string()
12
+ .describe('JSON object with field names as keys and values to set. Use null to clear a field. Example: {"leadParagraph": "New intro text", "htmlContent": null}'),
13
+ },
14
+ }, async ({ lNodeId, values: valuesJson }) => {
15
+ let values;
16
+ try {
17
+ values = JSON.parse(valuesJson);
18
+ }
19
+ catch {
20
+ return {
21
+ content: [{ type: "text", text: "Invalid JSON for values parameter." }],
22
+ isError: true,
23
+ };
24
+ }
25
+ if (typeof values !== "object" || values === null) {
26
+ return {
27
+ content: [{ type: "text", text: "Values must be a JSON object." }],
28
+ isError: true,
29
+ };
30
+ }
31
+ await callApi(config, "updateFields", { lNodeId, values });
32
+ return {
33
+ content: [{ type: "text", text: `Fields updated successfully on "${lNodeId}".` }],
34
+ };
35
+ });
36
+ }
@@ -0,0 +1,43 @@
1
+ ## Updating Field Values
2
+
3
+ When using the `paroicms_update_fields` tool, different field types require different value formats:
4
+
5
+ ### Simple fields (string, number, boolean, date, time, dateTime)
6
+
7
+ ```json
8
+ {"myField": "value"}
9
+ ```
10
+
11
+ ### Rich text fields (Tiptap or Quill editor plugins)
12
+
13
+ Always send the content as **Markdown**:
14
+
15
+ ```json
16
+ {"myRichTextField": "# Title\n\nParagraph text with **bold** and *italic*."}
17
+ ```
18
+
19
+ ### Labeling fields (taxonomy terms like tags, categories)
20
+
21
+ Use the `{"t": [...]}` wrapper with an array of term node IDs:
22
+
23
+ ```json
24
+ {"myLabelingField": {"t": ["16", "17"]}}
25
+ ```
26
+
27
+ To clear all terms: `{"myLabelingField": {"t": []}}`
28
+
29
+ ### JSON fields
30
+
31
+ Use the `{"j": ...}` wrapper:
32
+
33
+ ```json
34
+ {"myJsonField": {"j": {"key": "value"}}}
35
+ ```
36
+
37
+ ### Clearing a field
38
+
39
+ Use `null`:
40
+
41
+ ```json
42
+ {"myField": null}
43
+ ```
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@paroicms/mcp-plugin",
3
+ "version": "0.2.0",
4
+ "description": "MCP (Model Context Protocol) plugin for ParoiCMS - enables AI assistants to search content",
5
+ "keywords": [
6
+ "paroicms",
7
+ "plugin",
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "ai"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://gitlab.com/paroi/opensource/paroicms.git",
15
+ "directory": "plugins/mcp-plugin"
16
+ },
17
+ "author": "Paroi Team",
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "main": "backend/dist/index.js",
21
+ "bin": {
22
+ "paroicms-mcp": "cli/dist/index.js"
23
+ },
24
+ "scripts": {
25
+ "build": "npm run build:backend && npm run build:cli",
26
+ "build:backend": "(cd backend && tsc)",
27
+ "build:backend:watch": "(cd backend && tsc --watch --preserveWatchOutput)",
28
+ "build:cli": "(cd cli && tsc)",
29
+ "build:cli:watch": "(cd cli && tsc --watch --preserveWatchOutput)",
30
+ "clear": "rimraf backend/dist/* cli/dist/*",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "~1.22.0",
36
+ "@paroicms/script-lib": "0.3.9",
37
+ "arktype": "~2.1.27",
38
+ "zod": "~3.25.76"
39
+ },
40
+ "peerDependencies": {
41
+ "@paroicms/public-anywhere-lib": "0",
42
+ "@paroicms/public-server-lib": "0"
43
+ },
44
+ "devDependencies": {
45
+ "@paroicms/public-anywhere-lib": "0.39.0",
46
+ "@paroicms/public-server-lib": "0.49.0",
47
+ "@types/node": "~24.10.1",
48
+ "rimraf": "~6.1.2",
49
+ "typescript": "~5.9.3",
50
+ "vitest": "~4.0.13"
51
+ },
52
+ "files": [
53
+ "backend/dist",
54
+ "cli/dist",
55
+ "cli/docs"
56
+ ]
57
+ }