onelnx
← Back to all posts

19 June 2026

Blogging in Contentful with Claude & MCP

Blog Image

The Future on Blogging has arrived

While it's easy to blog using services like Threads or Twitter. How do you make your blogging easier if you have an existing blog and can't simply migrate or change it??

Dependencies

You need the following in order for this to work:

  • Claude Code
  • nodeJs
  • pnpm (or a package manager)
  • WSL2+ / MACOS or Linux
  • Contenful CMA Token
  • PEXELS API Key for getting images: https://www.pexels.com/

Create the MCP Server

1. Initialise the Repo

Create a new folder somewhere on your machine using pnpm init and add the following to your packages.json file

22 lines
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": [
    "build"
  ],
  "bin": {
    "mcp-contentful": "./build/index.js"
  },
  "keywords": [],
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "contentful-management": "^12.5.1",
    "dotenv": "^17.4.2",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/node": "^25.9.3",
    "typescript": "^6.0.3"
  }

2. Create the index.ts

This is the juicy part create an index.ts file and add the following. Replace the specifics for your Contentful content types. I'll use mine to give you and idea:

207 lines
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { createClient } from 'contentful-management';
 
const CMA_TOKEN = [CMA_TOKEN_VALUE];
const SPACE_ID = [SPACE_ID];
const ENVIRONMENT_ID = [ENVIRONMENT_ID];
const PEXELS_API_KEY = [PEXEL_API_KEY];
 
const interface blogPost {
    slug: {
        "en-US": string;
    };
    title: {
        "en-US": string;
    };
    heading: {
        "en-US": string;
    };
    summary: {
        "en-US": string;
    };
    content: {
        "en-US": string;
    };
    publishDate: {
        "en-US": string;
    };
    image: {
        "en-US": {
            "sys": {
                "type": "Link",
                "linkType": "Asset",
                "id": string
            }
        }
    };
};
 
const server = new McpServer({
    name: "mcp-contentful",
    version: "1.0.0",
});
 
const client = createClient({
    accessToken: CMA_TOKEN,
    host: "api.contentful.com",
})
 
server.registerTool(
    "create_blogPost",
    {
        description: "Create a new blog post",
        inputSchema: {
            slug: z
                .string()
                .min(3)
                .max(100)
                .describe("The slug portion of the URL"),
            title: z
                .string()
                .min(3)
                .max(100)
                .describe("The blog post title and blog heading"),
            summary: z
                .string()
                .min(10)
                .max(2000)
                .describe("The summary of the blog post"),
            content: z
                .string()
                .min(100)
                .max(50000)
                .describe("The main body content of the blog post")
        },
    },
    async ({ slug, title, summary, content }) => {
        try {
            const url = `https://api.pexels.com/v1/search?query=${encodeURIComponent(summary)}&per_page=1`;
            const response = await fetch(url, {
                headers: {
                    Authorization: PEXELS_API_KEY,
                },
            });
 
            const data = await response.json();
 
            const imageUrl = data.photos[0].src.original;
 
            let asset = await client.asset.create({
                environmentId: ENVIRONMENT_ID,
                spaceId: SPACE_ID
            }, {
                fields: {
                    title: {
                        "en-US": title
                    },
                    description: {
                        "en-US": `Image for blog post "${title}"`
                    },
                    file: {
                        "en-US": {
                            contentType: "image/jpeg",
                            fileName: `${slug}.jpg`,
                            upload: imageUrl
                        }
                    }
                }
            });
 
            asset = await client.asset.processForAllLocales({
                spaceId: SPACE_ID,
                environmentId: ENVIRONMENT_ID
            },
                asset,
                {
                    processingCheckWait: 2000,
                    processingCheckRetries: 10
                }
            );
 
            await client.asset.publish({
                spaceId: SPACE_ID,
                environmentId: ENVIRONMENT_ID,
                assetId: asset.sys.id
            },
                asset
            );
 
            await client.entry.create<blogPost>({
                contentTypeId: "blogPost",
                environmentId: ENVIRONMENT_ID,
                spaceId: SPACE_ID
            }, {
                fields: {
                    slug: {
                        "en-US": slug
                    },
                    title: {
                        "en-US": title
                    },
                    heading: {
                        "en-US": title
                    },
                    summary: {
                        "en-US": summary
                    },
                    content: {
                        "en-US": content
                    },
                    publishDate: {
                        "en-US": new Date().toISOString()
                    },
                    image: {
                        "en-US": {
                            sys: {
                                type: "Link",
                                linkType: "Asset",
                                id: asset.sys.id
                            }
                        }
                    },
                }
            })
        }
        catch (error) {
            console.error("Error creating blog post:", error);
            return {
                content: [
                    {
                        type: "text",
                        text: `Blog post "${title}" not created`,
                    }
                ],
            };
        }
 
        return {
            content: [
                {
                    type: "text",
                    text: `Blog Title "${title}"`,
                },
                {
                    type: "text",
                    text: `Blog Slug "${slug}"`,
                },
                {
                    type: "text",
                    text: `Blog Draft URL "https://[YOUR_WEBSITE_PREVIEW_URL]/blog/${slug}" created successfully in draft`,
                }
            ],
        };
    }
);
 
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("Contentful MCP Server running on stdio");
}
 
main().catch((error) => {
    console.error("Fatal error in main():", error);
    process.exit(1);
});

3. Register the MCP server

Run the following command to scope the MCP server to user computer so that it can run from any Claude Code session:

claude mcp add --scope user --transport stdio mcp-contentful -- node [APPLICATION_FOLDER_PATH]/build/index.js

To confirm that it's registered successfully you can run the following command to see the registered MCP servers on your local:

claude mcp list

Run that bad bouy

Perform a build to generate the index.js file that Claude Code is expecting, i.e. run pnpm dev

You can then create a blog post directly in Claude. Open claude from the command line and being prompting:

 Create me a Contentful blog post about boxing gloves. Use the following slug boxing-gloves-are-noice. Add the following title: "Boxing gloves are great and have many uses". Generate some content which I'll update later and use some of that as the summary

You should see an output similar to the following, note the Called mcp-contentful line shown at x2 speed

Next steps are to run /remote-control and then starting blogging from your phone.