How Category Tools Work for the Sitecore Ordercloud MCP Server : Structuring Catalogs with AI
Introduction: Why Categories Matter
If catalogs are storefronts, categories are the aisles and shelves that give shoppers context. In OrderCloud, categories serve multiple critical functions:
- Organize products inside each catalog (hierarchies, landing pages, curated groupings)
- Control navigation structure and faceted browsing
- Support business rules (channel-specific assortments, merchandised groupings, parent/child flows)
- Influence pricing, promotions, and personalization when paired with assignments
Every catalog can contain dozens or hundreds of categories spread across multiple levels. Keeping those structures tidy is tedious with raw REST calls, so we exposed the same functionality to AI tooling.
The Goal: Conversational Category Management
Manual category management typically requires:
- Remembering deeply nested REST endpoints (e.g.,
POST /v1/catalogs/{catalogId}/categories) - Working with PascalCase payloads, special flags like
adjustListOrders, and assignment schemas - Handling authentication, validation, and relationship cleanup
We wanted assistants to accept plain-language instructions such as:
“Add a ‘Sneakers’ child category under ‘Footwear’ in the Retail catalog, set it active, and assign it to the VIP buyer.”
The tooling handles schema validation, transformation, and API orchestration automatically.
Architecture: The Three-Layer Model
Our category tools follow a clean separation of concerns using three distinct layers:
Layer 1: Tool Registration
The registerCategoryTools function wires up every action the AI can invoke. Each tool describes:
- Title and description for the MCP manifest
- Zod schemas that validate user intent (e.g.,
catalogId,parentId, paging options) - A handler that transforms camelCase input into the PascalCase OrderCloud expects
- Structured responses and graceful error handling
Here’s an example of the List Categories tool registration:
server.registerTool(
"list_categories",
{
title: "List Categories",
description: "Retrieve a list of categories from a catalog",
inputSchema: {
catalogId: z.string().describe("The ID of the catalog"),
search: z.string().optional(),
searchOn: z.array(z.string()).optional(),
sortBy: z.array(z.string()).optional(),
page: z.number().optional(),
pageSize: z.number().optional(),
filters: z.record(z.string()).optional()
},
},
async (input) => {
const { catalogId, ...options } = input
const result = await orderCloudClient.categories.listCategories(
catalogId,
options
)
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}],
}
}
)
- Zod validation shields the API from malformed data before any HTTP call
- Input transformation converts camelCase fields into OrderCloud’s PascalCase payloads
- adjustListOrders flag support lets assistants reorder siblings without manual cleanup
- Error surface area is minimized by catching thrown errors and returning
isError: trueresponses
Complete Tool Inventory
The category toolkit exposes 17 operations grouped by feature:
Discovery & Legacy Compatibility
list_categories— paging, search, sort, filtersget_categories— legacy helper that forwards tolist_categoriesget_category— fetch a single node
CRUD Operations
create_categoryupdate_category(PUT)patch_category(PATCH)delete_category
Buyer Assignments
list_category_assignmentssave_category_assignmentdelete_category_assignment
Bundle Relationships
list_category_bundle_assignmentssave_category_bundle_assignmentdelete_category_bundle_assignment
Product Merchandising
list_category_product_assignmentssave_category_product_assignmentdelete_category_product_assignment
Layer 2: Category Client
The CategoryClient extends BaseClient, inheriting token injection and HTTP plumbing. It adds category-specific helpers with consistent logging via DebugLogger.
Pagination, Search, Sort & Filters
async listCategories(
catalogId: string,
options?: ListOptions
): Promise<ListResponse<Category>> {
this.ensureAuthenticated()
const params: any = {}
if (options?.search) {
params.search = options.search
}
if (options?.searchOn) {
params.searchOn = options.searchOn.join(",")
}
if (options?.sortBy) {
params.sortBy = options.sortBy.join(",")
}
if (options?.page) {
params.page = options.page
}
if (options?.pageSize) {
params.pageSize = options.pageSize
}
if (options?.filters) {
Object.entries(options.filters).forEach(([key, value]) => {
params[`filters[${key}]`] = value
})
}
const response = await this.client.get(
`v1/catalogs/${catalogId}/categories`,
{ params }
)
return response.data
}
All list-style helpers follow the same pattern, building query strings once and reusing them for assignments, bundles, and products.
CRUD Operations Implementation
async createCategory(
catalogId: string,
category: Category,
adjustListOrders?: boolean
): Promise<Category> {
this.ensureAuthenticated()
const params = adjustListOrders
? { adjustListOrders: true }
: {}
const response = await this.client.post(
`v1/catalogs/${catalogId}/categories`,
category,
{ params }
)
return response.data
}
async updateCategory(
catalogId: string,
categoryId: string,
category: Category
): Promise<Category> {
this.ensureAuthenticated()
const response = await this.client.put(
`v1/catalogs/${catalogId}/categories/${categoryId}`,
category
)
return response.data
}
async patchCategory(
catalogId: string,
categoryId: string,
partialCategory: Partial<Category>
): Promise<Category> {
this.ensureAuthenticated()
const response = await this.client.patch(
`v1/catalogs/${catalogId}/categories/${categoryId}`,
partialCategory
)
return response.data
}
async deleteCategory(
catalogId: string,
categoryId: string
): Promise<void> {
this.ensureAuthenticated()
await this.client.delete(
`v1/catalogs/${catalogId}/categories/${categoryId}`
)
}
Key features:
adjustListOrdersbubbles through to the API, enabling safe reorderingPartial<Category>typing keeps TypeScript honest for patch/update payloads- Authentication is enforced before every operation
Assignment, Bundle & Product Helpers
Dedicated methods encapsulate the slightly different endpoints:
// Category Assignments (Buyer access control)
async listCategoryAssignments(
catalogId: string,
options?: ListOptions
): Promise<ListResponse<CategoryAssignment>> {
// GET v1/catalogs/{catalogId}/categories/assignments
}
async saveCategoryAssignment(
catalogId: string,
assignment: CategoryAssignment
): Promise<void> {
// POST v1/catalogs/{catalogId}/categories/assignments
}
async deleteCategoryAssignment(
catalogId: string,
categoryId: string,
buyerId: string
): Promise<void> {
// DELETE v1/catalogs/{catalogId}/categories/{categoryId}/assignments
}
// Bundle Assignments
async listCategoryBundleAssignments(
catalogId: string,
options?: ListOptions
): Promise<ListResponse<CategoryBundleAssignment>> {
// GET v1/catalogs/{catalogId}/categories/bundleassignments
}
// Product Assignments
async listCategoryProductAssignments(
catalogId: string,
options?: ListOptions
): Promise<ListResponse<CategoryProductAssignment>> {
// GET v1/catalogs/{catalogId}/categories/productassignments
}
Bundled and product assignments mirror this shape, so AI tooling doesn’t need to memorize raw URLs.
Layer 3: OrderCloud API
The relevant OrderCloud REST endpoints include:
GET /v1/catalogs/{catalogId}/categoriesPOST /v1/catalogs/{catalogId}/categoriesPUT /v1/catalogs/{catalogId}/categories/{categoryId}PATCH /v1/catalogs/{catalogId}/categories/{categoryId}DELETE /v1/catalogs/{catalogId}/categories/{categoryId}- Assignment endpoints under
/categories/assignments,/bundleassignments,/productassignments
The tooling never exposes bearer tokens or raw Axios responses—everything is wrapped in structured MCP payloads.
Data Flow Example
User request:
“Create an ‘Outerwear’ category under ‘Apparel’ in the Retail catalog, make it active, move it to list order 5, and assign it to the Wholesale buyer.”
Step-by-Step Execution
1. Intent Parsing
AI extracts two operations:
- Create category (with parent, active flag, list order, adjustListOrders)
- Save category assignment targeting Wholesale buyer
2. Tool Calls
// First tool call
create_category({
catalogId: "retail",
name: "Outerwear",
parentId: "apparel",
active: true,
listOrder: 5,
adjustListOrders: true
})
// Second tool call
save_category_assignment({
catalogId: "retail",
categoryId: "<newly-created-id>",
buyerId: "wholesale-buyer"
})
3. Handler Transforms
- Converts camelCase fields (
parentId) into PascalCase for payloads - Includes
params: { adjustListOrders: true }so siblings shift automatically
4. Client Requests
// HTTP Request 1
POST /v1/catalogs/retail/categories?adjustListOrders=true
Content-Type: application/json
{
"Name": "Outerwear",
"ParentID": "apparel",
"Active": true,
"ListOrder": 5
}
// HTTP Request 2
POST /v1/catalogs/retail/categories/assignments
Content-Type: application/json
{
"CategoryID": "outerwear-123",
"BuyerID": "wholesale-buyer"
}
Advanced Capabilities
Hierarchy Management
The combination of parentId and adjustListOrders handles tree reshaping without manual cleanup:
// Move "Winter Jackets" to be first child under "Outerwear"
update_category({
catalogId: "retail",
categoryId: "winter-jackets",
parentId: "outerwear",
listOrder: 1,
adjustListOrders: true
})
Extended Properties (xp)
All create/update/patch tools pass through arbitrary metadata objects for custom experiences:
create_category({
catalogId: "retail",
name: "Premium Collection",
xp: {
seoTitle: "Exclusive Premium Products",
heroImageUrl: "https://cdn.example.com/premium-hero.jpg",
featuredUntil: "2024-12-31",
tags: ["luxury", "exclusive", "limited-edition"]
}
})
Rich Query Support
Every list tool supports comprehensive filtering and sorting:
list_categories({
catalogId: "retail",
search: "summer",
searchOn: ["Name", "Description"],
sortBy: ["Name", "!ListOrder"],
filters: {
Active: "true",
"xp.featured": "true"
},
page: 1,
pageSize: 20
})
Error Handling & Observability
The system includes comprehensive error handling and logging:
- Tool-level: Tools catch errors and reply with
isError: trueplus friendly messages - Client-level:
CategoryClientlogs start/success/error markers viaDebugLogger - Authentication:
ensureAuthenticated()is called before every HTTP request
try {
const result = await orderCloudClient.categories.createCategory(
catalogId,
categoryData
)
return {
content: [{
type: "text",
text: `Category created: ${result.ID}`
}]
}
} catch (error) {
return {
content: [{
type: "text",
text: `Error creating category: ${error.message}`
}],
isError: true
}
}
Real-World Scenarios
Navigation Redesign
Search for categories containing “Summer”, patch them to inactive, and reorder the remainder:
// Step 1: Find summer categories
const summerCategories = await list_categories({
catalogId: "retail",
search: "Summer"
})
// Step 2: Deactivate them
for (const category of summerCategories.Items) {
await patch_category({
catalogId: "retail",
categoryId: category.ID,
active: false
})
}
// Step 3: Reorder remaining active categories
await update_category({
catalogId: "retail",
categoryId: "fall-collection",
listOrder: 1,
adjustListOrders: true
})
Buyer-Specific Views
Query category assignments filtered by buyer, remove stale assignments, and add new ones:
// Find all VIP buyer assignments
const assignments = await list_category_assignments({
catalogId: "retail",
filters: { BuyerID: "vip-buyer" }
})
// Remove outdated assignments
await delete_category_assignment({
catalogId: "retail",
categoryId: "clearance",
buyerId: "vip-buyer"
})
// Add new exclusive category
await save_category_assignment({
catalogId: "retail",
categoryId: "exclusive-collection",
buyerId: "vip-buyer"
})
Product Curation
Assign top-selling SKUs to a featured category:
const topProducts = ["SKU-001", "SKU-045", "SKU-123"]
for (const productId of topProducts) {
await save_category_product_assignment({
catalogId: "retail",
categoryId: "best-sellers",
productId: productId
})
}
Bundle Promotion
Attach bundles to categories for seasonal campaigns:
await save_category_bundle_assignment({
catalogId: "retail",
categoryId: "holiday-gift-sets",
bundleId: "winter-bundle-2024"
})
Best Practices
- Keep catalog IDs consistent across tool calls; every category action is scoped to a single catalog
- Use adjustListOrders:true when reordering siblings to avoid duplicate order values
- Prefer patch_category for incremental tweaks (status flips, XP edits) to reduce payload size and race conditions
- Use filters and pagination when auditing large catalogs to stay within API limits:
filters: { Active: "true" }, page: 1, pageSize: 100 - Clean up downstream relationships (assignments, bundle links, product links) before deleting a category to avoid orphaned merchandising structures
Quick Reference
Example Tool Invocations
// Create child category with automatic reordering
create_category({
catalogId: "retail",
name: "Sneakers",
parentId: "footwear",
active: true,
listOrder: 2,
adjustListOrders: true
})
// Find all active child categories under footwear
list_categories({
catalogId: "retail",
search: "footwear",
filters: { Active: "true" }
})
// Assign VIP-only access
save_category_assignment({
catalogId: "retail",
categoryId: "sneakers",
buyerId: "vip-buyer"
})
// Update category with extended properties
patch_category({
catalogId: "retail",
categoryId: "sneakers",
xp: {
featured: true,
promoCode: "SNEAKER20"
}
})
// List all products in a category
list_category_product_assignments({
catalogId: "retail",
filters: { CategoryID: "sneakers" },
page: 1,
pageSize: 50
})
Implementation Files
For complete implementation details, see:
src/tools/categories/categories-tools.ts— Tool registration and MCP interfacesrc/tools/clients/categories-client.ts— HTTP client and business logic


