Execute Contentstack tool calls
A Contentstack tool call becomes an API request through the tool mapper.
This chapter covers the request-building logic:
resolve the API path
add query parameters
add mapped headers
build REST or GraphQL bodies
resolve the correct regional base URL
execute the request
Create src/execute-contentstack-tool.ts:
import type { ContentstackToolDefinition } from "./contentstack-tools.js";
import { getAuthHeaders } from "./auth.js";
import { getBaseUrl } from "./regions.js";
export async function executeContentstackTool(
definition: ContentstackToolDefinition,
args: Record<string, unknown>
) {
const region = process.env.CONTENTSTACK_REGION ?? "NA";
const baseUrl = await getBaseUrl(
definition.group,
region,
definition.subGroup
);
const path = resolvePath(
definition.mapper.apiUrl,
definition.mapper.params,
args
);
const query = buildQueryString(definition.mapper.queryParams, args);
const mappedHeaders = buildMappedHeaders(definition.mapper.headers, args);
const authHeaders = getAuthHeaders(definition.group);
const headers: Record<string, string> = {
...authHeaders,
...mappedHeaders,
"content-type": "application/json"
};
let body: unknown;
if (definition.mapper.type === "graphql") {
body = buildGraphQLBody(definition, args);
} else if (definition.mapper.type === "complex") {
body = buildComplexBody(definition.mapper.body, args);
} else if (typeof definition.mapper.body === "string") {
body = buildSimpleBody(definition.mapper.body, args);
}
const response = await fetch(`${baseUrl}${path}${query}`, {
method: definition.mapper.method,
headers,
body:
definition.mapper.method === "GET" || body === undefined
? undefined
: JSON.stringify(body)
});
const responseText = await response.text();
let data: unknown;
try {
data = JSON.parse(responseText);
} catch {
data = responseText;
}
if (!response.ok) {
throw new Error(
`Contentstack API request failed: ${response.status} ${response.statusText}\n${responseText}`
);
}
return data;
}Now add the helper functions.
Resolve path parameters
Some API URLs include placeholders.
Example mapper:
{
"apiUrl": "/v3/content_types/content_type_uid/entries",
"params": {
"content_type_uid": "content_type_uid"
}
}Example tool arguments:
{
"content_type_uid": "blog_post"
}Resolved path:
/v3/content_types/blog_post/entriesImplementation:
function resolvePath(
apiUrl: string,
params: Record<string, string> | undefined,
args: Record<string, unknown>
) {
let path = apiUrl;
for (const [placeholder, argName] of Object.entries(params ?? {})) {
const value = args[argName];
if (value === undefined || value === null) {
throw new Error(`Missing required path parameter: ${argName}`);
}
path = path.replaceAll(placeholder, encodeURIComponent(String(value)));
}
return path;
}Build query parameters
Some mapper fields become query parameters.
Example mapper:
{
"queryParams": {
"limit": "limit",
"skip": "skip",
"include_count": "include_count"
}
}Implementation:
function buildQueryString(
queryParams: Record<string, string> | undefined,
args: Record<string, unknown>
) {
const searchParams = new URLSearchParams();
for (const [queryKey, argName] of Object.entries(queryParams ?? {})) {
const value = args[argName];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
searchParams.append(queryKey, String(item));
}
} else if (typeof value === "object") {
searchParams.set(queryKey, JSON.stringify(value));
} else {
searchParams.set(queryKey, String(value));
}
}
const query = searchParams.toString();
return query ? `?${query}` : "";
}Build mapped headers
Some arguments become headers. A common example is branch.
Example mapper:
{
"headers": {
"branch": "branch"
}
}Implementation:
function buildMappedHeaders(
headers: Record<string, string> | undefined,
args: Record<string, unknown>
) {
const result: Record<string, string> = {};
for (const [headerName, argName] of Object.entries(headers ?? {})) {
const value = args[argName];
if (value !== undefined && value !== null) {
result[headerName] = String(value);
}
}
return result;
}Build simple request bodies
For simple REST tools, the mapper can define a string body key.
Example mapper:
{
"body": "entry_data"
}Example arguments:
{
"entry_data": {
"entry": {
"title": "Hello from MCP"
}
}
}Request body:
{
"entry": {
"title": "Hello from MCP"
}
}Implementation:
function buildSimpleBody(
bodyKey: string | undefined,
args: Record<string, unknown>
) {
if (!bodyKey) {
return undefined;
}
return args[bodyKey];
}Build complex request bodies
Some tools reconstruct nested request bodies from flat arguments. These tools use type: "complex" and x-mapFrom.
Implementation:
function buildComplexBody(schema: any, args: Record<string, unknown>): unknown {
if (!schema || typeof schema !== "object") {
return undefined;
}
if (schema["x-mapFrom"]) {
return args[schema["x-mapFrom"]];
}
if (schema.type === "object" || schema.properties) {
const result: Record<string, unknown> = {};
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
const value = buildComplexBody(childSchema, args);
if (value !== undefined) {
result[key] = value;
}
}
return result;
}
if (schema.type === "array" && schema.items) {
const value = buildComplexBody(schema.items, args);
if (value === undefined) {
return undefined;
}
return Array.isArray(value) ? value : [value];
}
return undefined;
}Resolve regional base URLs
Contentstack APIs are regional. Your server should resolve the correct base URL for the selected region.
Create src/regions.ts:
type RegionKey = string;
export async function getBaseUrl(
group: string,
region: RegionKey,
subGroup?: string
) {
if (group === "lytics") {
return "https://api.lytics.io";
}
const response = await fetch("https://artifacts.contentstack.com/regions.json");
if (!response.ok) {
throw new Error("Failed to fetch Contentstack regions");
}
const regions = await response.json();
const regionConfig = regions[region];
if (!regionConfig) {
throw new Error(`Unknown Contentstack region: ${region}`);
}
if (group === "cma" || group === "cma-extended") {
return regionConfig.contentManagement;
}
if (group === "cda") {
return regionConfig.contentDelivery;
}
if (group === "developerhub") {
return regionConfig.developerHub;
}
if (group === "personalize") {
return regionConfig.personalizeManagement;
}
if (group === "analytics") {
return `${regionConfig.application}/analytics`;
}
if (group === "launch") {
return `${regionConfig.launch}/manage`;
}
if (group === "brandkit" && subGroup === "brand-kits-api") {
return regionConfig.brandKit;
}
if (group === "brandkit" && subGroup === "ai") {
return regionConfig.genAI.replace(/\/brand-kits$/, "");
}
throw new Error(`Unsupported Contentstack tool group: ${group}`);
}Common group mappings are:
Tool group | Region field | Extra path suffix |
cma | contentManagement | none |
cma-extended | contentManagement | none |
cda | contentDelivery | none |
developerhub | developerHub | none |
personalize | personalizeManagement | none |
analytics | application | /analytics |
launch | launch | /manage |
Frequently asked questions
What does executeContentstackTool do?
It turns a tool definition plus arguments into a Contentstack API request by resolving the path, query, headers, body, base URL, and executing fetch.
How are path placeholders resolved in API URLs?
resolvePath replaces placeholders using mapper params and tool arguments, URL-encoding values and throwing an error when a required argument is missing.
How are query parameters built from tool arguments?
buildQueryString maps arguments to query keys, skipping null/undefined, appending array values, JSON-stringifying objects, and producing a URLSearchParams string.
How do REST, complex, and GraphQL request bodies get built?
REST uses a simple body key, complex bodies are reconstructed via schema properties and x-mapFrom, and GraphQL uses a dedicated builder when mapper.type is "graphql".
How is the correct regional Contentstack base URL selected?
getBaseUrl fetches regions.json and chooses the right endpoint field based on the tool group (and subGroup where needed), adding path suffixes for some groups.