This is a preview of the Storyblok Website with Draft Content

Bring your Storyblok data into Claude by coding an MCP server

Storyblok is the first headless CMS that works for developers & marketers alike.

LLMs are all the rage right now, so unless you’re living under a rock, you probably have used one recently. LLMs have become very efficient for general-purpose knowledge work and are able to efficiently help with coding-related tasks as well. Claude is one such family of Large Language Models and a chat client of the same name.

What is an MCP?

Although some LLMs have the functionality of browsing the internet, which means they are able to use up-to-date resources, one thing LLM clients lack is context and connection with internal tooling. For example, if I want Claude to run an analysis based on visitor analytics of my website, I’d have to first export the data from my analytics platform and then feed it into Claude. This is cumbersome, and the data can get outdated pretty fast.

Enter MCP.

The Model Context Protocol ( MCP ) is a standardized way of feeding context to LLMs. Here’s a snippet from the official resource on MCPs to help you understand:

Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.

The MCP is an evolving standard, with Claude Desktop being one of the clients with first-party support.

Before we proceed further, let’s refine our understanding of how MCP integrates with LLM clients. There are two main nodes that send and receive information:

  • MCP client: The LLM client or chat application is also the one that communicates to external data sources to load context and perform additional actions outside of the realm of the client. In our case, the MCP client is the Claude Desktop application.
  • MCP server: This is a web server that is either running locally or on a remote host. It implements the MCP specification and exposes various tools that the client can use.

For example, based on my earlier use case of Claude having up-to-date access to my analytics data, I would have to first write an MCP server that extracts my analytics and returns it as a response for the Claude Desktop client to consume.

Imagine asking Claude to give you a list of your most visited pages, along with some advanced filtering. Claude fetches this information behind the scenes without you doing it manually. Efficient, Right? This is possible when using an MCP.

Building an MCP Server for your Storyblok data

Having direct access to your internal data, tooling, and context within an LLM client is very useful. It would unlock a myriad of use cases if you want to use AI for your content authoring and management needs. Let us now try to build an MCP server, which will allow Claude to have access to data in a Storyblok space. We will try to fulfill the following use case:

I have a blog using Storyblok as a CMS, and I have to “intelligently” add tags to my blog posts. Each blog post is an individual story.

INFO:

This tutorial assumes you are familiar with Typescript. Make sure you have the current LTS version of Node installed.

Getting started

Skim through the official quickstart guide for creating an MCP server. The guide is very detailed, but reading it will make you comfortable when we code the Storyblok MCP server. Let’s start with a barebones MCP template by cloning this repo.

git clone https://github.com/arpitbatra123/mcp-skeleton.git


Once that is done, open the project in the editor of your choice. The core logic of the MCP server is in src/index.ts Let’s unravel what’s in there.

...
const server = new McpServer({
  name: "skeleton-manager",
  version: "1.0.0",
});
...

The above section initializes the MCP server. McpServer is imported from the @modelcontextprotocol package.

...
async function main() {
  try {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("MCP server is running");
  } catch (error) {
    console.error("Error starting server:", error);
    process.exit(1);
  }
}

main();
...


The main function, sets up the server to accept input from StdioServerTransport , which in our case, is the Claude client.

Let us move on to the “tool” declaration now. A tool in the context of an MCP server essentially exposes functionality to the MCP client. We can think of it as a function that will be called when the MCP client needs specific information from the MCP server. A “tool” can accept input and send output to the client, and can also perform programmatic actions involving CRUD of resources.

In our current code, I’ve set up a “ping” tool to test whether our server is running successfully:

...
server.tool(
  "ping",
  "Ping the skeleton server to check if it is running",
  { message: z.string().describe("Your Message") },
  async ({ message }) => {
...

Let’s build an executable out of our sample code and link it with the Claude Desktop client.

npm install
npm run build

The above commands will build our code into a JS executable and output it into the build folder.

Now, follow the "Testing your server with Claude for Desktop” section of the official guide to integrate the MCP server with your Claude Desktop client.

warn:

The Claude Desktop client must be restarted after a server config change and whenever you modify the server code.

We can now try to ping our server from Claude Desktop. Start a new chat in Claude and send the following message - “Ping "Hello World" ”

Pinging the MCP Server

Notice how Claude has identified that this query needs external context and invoked the MCP server's tool. Since MCP servers can run arbitrary code, you have to approve the tool's execution. Once you do so, the ping tool will relay your message, which confirms that our server is up and running!

MCP Server Ping Reply

Setting up tools for Storyblok

Now that our basic server is running, we can add additional tools so that Claude Desktop can talk to our Storyblok resources. We will do this by making use of the Storyblok API. For our use case of auto-tagging our stories, we would need the following tools:

  • Fetch Stories
  • Read Tags in the Space
  • Add/Update Story Tags

For brevity, I will limit my tool scope to a single space and hardcode it in my code:

const STORYBLOK_SPACE_ID = XXXXXX;

For this demo, I’ve already set up a space on Storyblok with some sample blog posts that need “intelligent” tagging.

We will communicate with Storyblok using the Management API, so let’s get a token for it <LINK> and include it in our code as a global.

const STORYBLOK_MANAGEMENT_TOKEN = "your-token-here";
warn:

Remember not to push your code in its current form to version control, as this will expose your token. We are hardcoding it here only to make things work first. The token should ideally be in an .env file.

Let’s add our first tool, which will be used to fetch stories:

...
server.tool(
  "fetch-stories",
  "Fetches stories from Storyblok",
  {},
  async ({}) => {
    try {
      const response = await fetch(
        `https://mapi.storyblok.com/v1/spaces/${STORYBLOK_SPACE_ID}/stories/`,
        {
          headers: {
            Authorization: `${STORYBLOK_MANAGEMENT_TOKEN}`,
          },
        }
      );
      if (!response.ok) {
        throw new Error(`Failed to fetch stories: ${response.statusText}`);
      }
      const data = await response.json();
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(data, null, 2),
          },
        ],
      };
    } catch (error) {
      console.error("Error in story fetch tool:", error);
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
      };
    }
  }
);
...

You can now ask Claude to fetch stories in your Storyblok space, and it should work:

stories being fetched from the MCP server

Notice how Claude has already started to extract insights regarding our stories from the response of the MCP server.

Let us now add two more tools: one to get tags in your Storyblok space and another to create a new tag and add it to a story.

The tool below will fetch the tags you currently have in your Storyblok space:

...
server.tool(
  "fetch-tags",
  "Fetches tags in your Storyblok space",
  {},
  async ({}) => {
    try {
      const response = await fetch(
        `https://mapi.storyblok.com/v1/spaces/${STORYBLOK_SPACE_ID}/tags/`,
        {
          headers: {
            Authorization: `${STORYBLOK_MANAGEMENT_TOKEN}`,
          },
        }
      );
      if (!response.ok) {
        throw new Error(`Failed to fetch tags: ${response.statusText}`);
      }
      const data = await response.json();
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(data, null, 2),
          },
        ],
      };
    } catch (error) {
      console.error("Error in tags fetch:", error);
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
      };
    }
  }
);
...

This one will create a new tag and subsequently add it to a story:

...
server.tool(
  "create-tag-and-add-to-story",
  "Create a new tag in your Storyblok space and add it to a story",
  {
    name: z.string().describe("The name of the tag to create"),
    id: z.string().describe("The story id to add the tag to"),

  },
  async ({name, id}) => {
    try {
      const response = await fetch(
        `https://mapi.storyblok.com/v1/spaces/${STORYBLOK_SPACE_ID}/tags/`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `${STORYBLOK_MANAGEMENT_TOKEN}`,
          },
          body: JSON.stringify({ name, story_id: id }),
        }
      );
      if (!response.ok) {
        throw new Error(`Failed to create tag: ${response.statusText}`);
      }
      const data = await response.json();
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(data, null, 2),
          },
        ],
      };
    } catch (error) {
      console.error("Error in tag create:", error);
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
      };
    }
  }
);
...

Notice how the 3rd argument passed to server.tool contains arguments for the tool. You can use natural language to pass these arguments to the tool via Claude.

learn:

Note: For this demo, I will ask Claude to suggest a unique tag for each of my stories, create the new tag, and add it to the story. If you wanted to add existing tags, you’d have to write a new tool that fetches the story and then updates the tags for that specific story.

Once that is done, rebuild your code using npm run build and then restart Claude.

Instructing Claude to Suggest and Add Tags to Stories

Now that all our tools are set up, we can turn to Claude for assistance with the task at hand.

Let’s first ask Claude to suggest a unique tag for each of our blog posts:

claude suggesting tags for my storiesGreat. Based on the titles of our posts, Claude has suggested a unique tag for each one.

Now, we can add these tags to the posts. Let’s instruct Claude to “use the post ID to add the tags to the posts.”

It will run the tools we just added and use the Storyblok Management API to update the posts' tags.

claude tagging stories using the MCP serverAll done! We used Claude’s AI Capabilities to suggest tags for our content, and thanks to our MCP server, Claude could add those tags to the stories all from within the tool itself.

Caveats

The MCP is evolving, and enterprise requirements such as sandboxing, authentication, and hosting are still being standardized. Additionally, the one-to-one communication model is most effective in local, single-user contexts and does not scale well. Please keep these caveats and potential security issues in mind before granting an MCP access to your Storyblok space.

Final Thoughts

What we just did is just a subset of what this setup enables. Imagine using AI for your content creation and management needs, with your latest stories and Storyblok readily available to your LLM Client. MCP doesn’t just work with chat-based clients; they can already interface with coding assistants such as Cursor, which means you can design a Storyblok schema, make that schema available to your coding assistant through an MCP, and then use this setup to code your components faster.

Additional resources

Author

Arpit Batra

Arpit Batra

Arpit Batra is a front-end engineer with a background in creating user-friendly interfaces. He now applies his eye for detail and passion for clear communication to crafting comprehensive technical documentation for Storyblok.