Note that the code samples are illustrative
The code samples in this chapter are illustrative and not production ready. Feel free to point your AI agent to the markdown version of this full guide to help you create a real MCP server.
Build the MCP server
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 -yInstall dependencies:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/nodeCreate a TypeScript config:
npx tsc --initAdd a development script to package.json:
{
"scripts": {
"dev": "tsx src/server.ts"
}
}Create the source directory:
mkdir srcCreate 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 devYou 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.