Skip to main content
Our docs redesign is live!

Build the MCP server

3 min read

In this chapter, you will create a Node.js MCP server, fetch Contentstack tool definitions, and register them as MCP tools.

Start by creating a project:

mkdir contentstack-custom-mcp
cd contentstack-custom-mcp
npm init -y

Install dependencies:

npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Create a TypeScript config:

npx tsc --init

Add a development script to package.json:

{
  "scripts": {
    "dev": "tsx src/server.ts"
  }
}

Create the source directory:

mkdir src

Create src/server.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "contentstack-custom-mcp",
  version: "1.0.0"
});

server.tool(
  "ping",
  "Check that the custom Contentstack MCP server is running.",
  {},
  async () => {
    return {
      content: [
        {
          type: "text",
          text: "Contentstack MCP server is running."
        }
      ]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Run the server:

npm run dev

You now have a minimal MCP server with one test tool.

Next, add support for fetching Contentstack tool definitions.

Create src/contentstack-tools.ts:

export type ContentstackToolDefinition = {
  name: string;
  description?: string;
  group: string;
  subGroup?: string;
  inputSchema: Record<string, unknown>;
  mapper: {
    apiUrl: string;
    method: "GET" | "POST" | "PUT" | "DELETE";
    type?: "complex" | "graphql" | "object" | string;
    params?: Record<string, string>;
    queryParams?: Record<string, string>;
    headers?: Record<string, string>;
    body?: string | Record<string, unknown>;
    query?: string;
    variables?: Record<string, { type?: string; "x-mapFrom"?: string }>;
  };
};

export async function fetchToolDefinitions(group: string) {
  const response = await fetch(`https://mcp.contentstack.com/${group}/tools`);

  if (!response.ok) {
    throw new Error(
      `Failed to fetch Contentstack tools for group "${group}": ${response.status}`
    );
  }

  return (await response.json()) as Record<string, ContentstackToolDefinition>;
}

Update src/server.ts to load Contentstack tools:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { fetchToolDefinitions } from "./contentstack-tools.js";
import { executeContentstackTool } from "./execute-contentstack-tool.js";

const server = new McpServer({
  name: "contentstack-custom-mcp",
  version: "1.0.0"
});

const groups = ["cda"];

const definitions = Object.assign(
  {},
  ...(await Promise.all(groups.map(fetchToolDefinitions)))
);

You can expose all tools in a group, but it is safer to allowlist only the tools you need.

For example:

const allowedToolNames = new Set([
  "get_all_assets_cdn"
]);

const allowedDefinitions = Object.fromEntries(
  Object.entries(definitions).filter(([toolName]) =>
    allowedToolNames.has(toolName)
  )
);

Then register each allowlisted Contentstack tool with your MCP server:

for (const [toolName, definition] of Object.entries(allowedDefinitions)) {
  server.tool(
    toolName,
    definition.description ?? `Contentstack tool: ${toolName}`,
    definition.inputSchema as any,
    async (args: Record<string, unknown>) => {
      const result = await executeContentstackTool(definition, args);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(result, null, 2)
          }
        ]
      };
    }
  );
}

const transport = new StdioServerTransport();
await server.connect(transport);

At this point, your MCP server can fetch tool definitions and register them as MCP tools. The next step is to implement executeContentstackTool.

Frequently asked questions

  • What does this guide build?

    A minimal Node.js MCP server in TypeScript that starts with a ping tool, then dynamically loads and registers Contentstack tools.

  • How are Contentstack tool definitions fetched?

    They are fetched over HTTP from https://mcp.contentstack.com/{group}/tools using a group name such as "cda" and parsed as JSON.

  • Why should I allowlist Contentstack tools before registering them?

    Allowlisting limits the exposed surface area to only the tools you need, reducing unintended capabilities and making behavior easier to control.

  • How are Contentstack tools registered with the MCP server?

    Each allowlisted tool is added via server.tool(toolName, description, inputSchema, handler), where the handler calls executeContentstackTool and returns text content.

  • What transport does the MCP server use in this setup?

    It uses StdioServerTransport, which connects the MCP server over standard input/output for local tool execution.