Skip to main content
Our docs redesign is live!

Execute Contentstack tool calls

5 min read

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/entries

Implementation:

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.