Skip to main content
Glama

Sitecore MCP Server

by GaryWenneker
sitecore-service.ts61.2 kB
import axios, { AxiosInstance } from 'axios'; import https from 'https'; export interface SitecoreItem { id: string; name: string; displayName: string; path: string; templateId: string; templateName: string; language: string; version: number; fields: Record<string, any>; hasChildren: boolean; parentId?: string; created?: string; updated?: string; } interface GraphQLResponse { data?: any; errors?: Array<{ message: string }>; } export class SitecoreService { private client: AxiosInstance; private graphqlEndpoint: string; private apiKey: string; constructor( private sitecoreHost: string, private username: string, private password: string, apiKey?: string, endpoint?: string ) { // GraphQL API endpoint - configurable via parameter or environment variable // Default: /sitecore/api/graph/items/master // Alternative: /sitecore/api/graph/items/web (for published content) this.graphqlEndpoint = endpoint || process.env.SITECORE_ENDPOINT || `${this.sitecoreHost}/sitecore/api/graph/items/master`; this.apiKey = apiKey || process.env.SITECORE_API_KEY || ''; // Validate API key is present if (!this.apiKey) { throw new Error('SITECORE_API_KEY is required but not provided'); } // Create axios instance with authentication this.client = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false, // For local development with self-signed certs }), timeout: 30000, headers: { // These headers are REQUIRED for every GraphQL request sc_apikey: this.apiKey, 'Content-Type': 'application/json', }, }); // Add Basic Auth as fallback (optional) if (username && password) { const auth = Buffer.from(`${username}:${password}`).toString('base64'); this.client.defaults.headers.common['Authorization'] = `Basic ${auth}`; } } /** * Execute GraphQL query tegen Sitecore */ private async executeGraphQL(query: string, variables?: any): Promise<any> { try { const response = await this.client.post<GraphQLResponse>(this.graphqlEndpoint, { query, variables, }); if (response.data.errors) { throw new Error(`GraphQL Error: ${response.data.errors.map((e) => e.message).join(', ')}`); } return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { if (error.response?.data?.errors) { throw new Error( `GraphQL Error: ${error.response.data.errors.map((e: any) => e.message).join(', ')}` ); } throw new Error(`GraphQL API Error: ${error.message}`); } throw error; } } /** * Format GUID to Sitecore format with curly braces and dashes * CRITICAL: Sitecore GraphQL returns GUIDs WITHOUT dashes (CFFDFAFA317F4E5498988D16E6BB1E68) * but queries REQUIRE dashes ({CFFDFAFA-317F-4E54-9898-8D16E6BB1E68}) * * @param guid - GUID string with or without dashes/curly braces * @returns Formatted GUID: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} */ private formatGuid(guid: string): string { // Remove curly braces if present const cleanGuid = guid.replace(/[{}]/g, ''); // If already has dashes, just add curly braces if (cleanGuid.includes('-')) { return `{${cleanGuid}}`; } // Add dashes in 8-4-4-4-12 format if (cleanGuid.length === 32) { return `{${cleanGuid.substring(0, 8)}-${cleanGuid.substring(8, 12)}-${cleanGuid.substring(12, 16)}-${cleanGuid.substring(16, 20)}-${cleanGuid.substring(20, 32)}}`; } // If format is unexpected, return as-is with curly braces return `{${cleanGuid}}`; } /** * Determine smart language default based on path * SITECORE BEST PRACTICE: * - Templates, renderings, system items: ALWAYS 'en' * - Content items: Use specified language or 'en' as fallback * * HELIX ARCHITECTURE: * - Foundation layer: /sitecore/templates/Foundation * - Feature layer: /sitecore/templates/Feature * - Project layer: /sitecore/templates/Project * - All templates MUST be in 'en' language */ private getSmartLanguageDefault(path: string, specifiedLanguage?: string): string { // If language is explicitly specified, use it if (specifiedLanguage) { return specifiedLanguage; } // ALWAYS 'en' for: // - Templates (Helix: Foundation/Feature/Project) // - System items // - Layout/Renderings // - Media Library (optional, often 'en') const systemPaths = [ '/sitecore/templates', '/sitecore/layout', '/sitecore/system', '/sitecore/media library', ]; const isSystemPath = systemPaths.some((sp) => path.toLowerCase().startsWith(sp.toLowerCase())); if (isSystemPath) { return 'en'; // REQUIRED: Templates/Renderings are ALWAYS in 'en' } // For content items under /sitecore/content // Default to 'en' but allow override return 'en'; } /** * Get a single Sitecore item by path or ID * SMART DEFAULTS: * - Language: Auto-detect ('en' for templates/system, specified or 'en' for content) * - Version: Latest version unless specified * - Response includes: Total version count */ async getItem( path: string, language?: string, _database: string = 'master', version?: number ): Promise<SitecoreItem & { versionCount?: number }> { // Apply smart language default const effectiveLanguage = this.getSmartLanguageDefault(path, language); const query = ` query GetItem($path: String!, $language: String!, $version: Int) { item(path: $path, language: $language, version: $version) { id name displayName path template { id name } hasChildren language { name } version } } `; const result = await this.executeGraphQL(query, { path, language: effectiveLanguage, version, }); if (!result.item) { throw new Error( `Item not found: ${path} (language: ${effectiveLanguage}${version ? `, version: ${version}` : ''}). ` + `The item might exist in a different language version. ` + `Common languages: en, nl, nl-NL, de, fr. ` + `Tip: Use sitecore_get_children on the parent folder to see available items.` ); } const item = result.item; // Get total version count for this language let versionCount: number | undefined; try { const versions = await this.getItemVersions(path, effectiveLanguage); versionCount = versions.length; } catch { // If version count fails, don't block the response versionCount = undefined; } return { id: item.id, name: item.name, displayName: item.displayName, path: item.path, templateId: item.template.id, templateName: item.template.name, language: item.language?.name || effectiveLanguage, version: item.version || version || 1, hasChildren: item.hasChildren, fields: {}, // Fields moeten apart opgevraagd worden met getFieldValue versionCount: versionCount, }; } /** * Get children of a Sitecore item * NEW: Supports version parameter */ async getChildren( path: string, language: string = 'en', _database: string = 'master', _recursive: boolean = false, version?: number ): Promise<SitecoreItem[]> { const query = ` query GetChildren($path: String!, $language: String!, $version: Int) { item(path: $path, language: $language, version: $version) { children(first: 100) { id name displayName path template { id name } hasChildren } } } `; const result = await this.executeGraphQL(query, { path, language, version }); if (!result.item) { throw new Error(`Item not found: ${path}`); } // In /items/master schema is children een directe array van items const children = result.item.children || []; return children.map((child: any) => { return { id: child.id, name: child.name, displayName: child.displayName, path: child.path, templateId: child.template.id, templateName: child.template.name, language: language, version: 1, hasChildren: child.hasChildren, fields: {}, // Fields moeten apart opgevraagd worden per field }; }); } /** * Execute a Sitecore query (via GraphQL search) */ async executeQuery( queryPath: string, language: string = 'en', _database: string = 'master', maxItems: number = 100 ): Promise<SitecoreItem[]> { const query = ` query Search($path: String!, $language: String!, $first: Int!) { search( where: { name: "_path" value: $path operator: CONTAINS } first: $first language: $language ) { results { items { id name displayName path template { id name } hasChildren fields { name value } } } } } `; const result = await this.executeGraphQL(query, { path: queryPath, language, first: maxItems, }); const items = result.search?.results?.items || []; return items.map((item: any) => { const fields: Record<string, any> = {}; item.fields?.forEach((field: any) => { fields[field.name] = field.value; }); return { id: item.id, name: item.name, displayName: item.displayName, path: item.path, templateId: item.template.id, templateName: item.template.name, language: language, version: 1, hasChildren: item.hasChildren, fields, }; }); } /** * Search for items by name (BACKWARDS COMPATIBLE - returns just array) * NEW for /items/master: Enhanced with facets, field filtering, index selection * ENHANCED: Client-side filters (path_contains, name_contains, template_in, hasChildren, hasLayout) * ENHANCED: Client-side sorting (orderBy: name, displayName, path) * For pagination support, use searchItemsPaginated() instead */ async searchItems( searchText?: string, rootPath?: string, templateName?: string, language: string = 'en', _database: string = 'master', maxItems: number = 50, index?: string, fieldsEqual?: Array<{ field: string; value: string }>, facetOn?: string[], latestVersion?: boolean, filters?: { pathContains?: string; pathStartsWith?: string; nameContains?: string; templateIn?: string[]; hasChildrenFilter?: boolean; hasLayoutFilter?: boolean; }, orderBy?: Array<{ field: 'name' | 'displayName' | 'path'; direction: 'ASC' | 'DESC' }> ): Promise<SitecoreItem[]> { // NOTE: ContentSearchResult has DIFFERENT fields than Item! // ContentSearchResult fields: id, name, path, templateName, uri, language (String!) // Item fields: id, name, displayName, path, template, hasChildren, fields const query = ` query Search( $keyword: String $rootItem: String $language: String $first: Int $index: String $latestVersion: Boolean ) { search( keyword: $keyword rootItem: $rootItem language: $language first: $first index: $index latestVersion: $latestVersion ) { results { items { id name path templateName uri language } } } } `; const result = await this.executeGraphQL(query, { keyword: searchText, rootItem: rootPath, language, first: maxItems, index, latestVersion, }); let items = result.search?.results?.items || []; // Filter op templateName indien nodig if (templateName) { items = items.filter((item: any) => item.templateName === templateName); } // Apply enhanced client-side filters if (filters) { if (filters.pathContains) { items = items.filter((item: any) => item.path.toLowerCase().includes(filters.pathContains!.toLowerCase()) ); } if (filters.pathStartsWith) { items = items.filter((item: any) => item.path.toLowerCase().startsWith(filters.pathStartsWith!.toLowerCase()) ); } if (filters.nameContains) { items = items.filter((item: any) => item.name.toLowerCase().includes(filters.nameContains!.toLowerCase()) ); } if (filters.templateIn && filters.templateIn.length > 0) { items = items.filter((item: any) => filters.templateIn!.includes(item.templateName)); } if (filters.hasChildrenFilter !== undefined) { // Note: ContentSearchResult does NOT have hasChildren field // This filter will be ignored for search results console.warn('hasChildrenFilter is not supported by ContentSearchResult, filter ignored'); } if (filters.hasLayoutFilter !== undefined) { // Note: ContentSearchResult does NOT have fields array // This filter will be ignored for search results console.warn('hasLayoutFilter is not supported by ContentSearchResult, filter ignored'); } } // Apply client-side sorting if (orderBy && orderBy.length > 0) { items.sort((a: any, b: any) => { for (const sort of orderBy) { const aVal = a[sort.field] || ''; const bVal = b[sort.field] || ''; const comparison = aVal.localeCompare(bVal, undefined, { sensitivity: 'base' }); if (comparison !== 0) { return sort.direction === 'ASC' ? comparison : -comparison; } } return 0; }); } return items.map((item: any) => { // ContentSearchResult fields: id, name, path, templateName, uri, language (String) // Map to SitecoreItem interface return { id: item.id, name: item.name, displayName: item.name, // ContentSearchResult doesn't have displayName, use name path: item.path, templateId: '', // ContentSearchResult doesn't provide templateId templateName: item.templateName, language: item.language || language, // language is String, not object! version: 1, // ContentSearchResult doesn't provide version hasChildren: false, // ContentSearchResult doesn't have hasChildren fields: {}, // ContentSearchResult doesn't have fields array }; }); } /** * Search for items WITH PAGINATION SUPPORT * ENHANCED: Client-side filters (path_contains, name_contains, template_in, hasChildren, hasLayout) * ENHANCED: Client-side sorting (orderBy: name, displayName, path) * Returns items plus pagination metadata (pageInfo, totalCount) */ async searchItemsPaginated( searchText?: string, rootPath?: string, templateName?: string, language: string = 'en', _database: string = 'master', maxItems: number = 50, index?: string, fieldsEqual?: Array<{ field: string; value: string }>, facetOn?: string[], latestVersion?: boolean, after?: string, filters?: { pathContains?: string; pathStartsWith?: string; nameContains?: string; templateIn?: string[]; hasChildrenFilter?: boolean; hasLayoutFilter?: boolean; }, orderBy?: Array<{ field: 'name' | 'displayName' | 'path'; direction: 'ASC' | 'DESC' }> ): Promise<{ items: SitecoreItem[]; pageInfo: { hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string | null; endCursor: string | null; }; totalCount: number | null; }> { // NOTE: ContentSearchResult has DIFFERENT fields than Item! // ContentSearchResult fields: id, name, path, templateName, uri, language (String!) const query = ` query Search( $keyword: String $rootItem: String $language: String $first: Int $after: String $index: String $latestVersion: Boolean ) { search( keyword: $keyword rootItem: $rootItem language: $language first: $first after: $after index: $index latestVersion: $latestVersion ) { results { items { id name path templateName uri language } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } } `; const result = await this.executeGraphQL(query, { keyword: searchText, rootItem: rootPath, language, first: maxItems, after, index, latestVersion, }); let items = result.search?.results?.items || []; const pageInfo = result.search?.results?.pageInfo || { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null, }; const totalCount = result.search?.results?.totalCount || null; // Filter op templateName indien nodig if (templateName) { items = items.filter((item: any) => item.templateName === templateName); } // Apply enhanced client-side filters if (filters) { if (filters.pathContains) { items = items.filter((item: any) => item.path.toLowerCase().includes(filters.pathContains!.toLowerCase()) ); } if (filters.pathStartsWith) { items = items.filter((item: any) => item.path.toLowerCase().startsWith(filters.pathStartsWith!.toLowerCase()) ); } if (filters.nameContains) { items = items.filter((item: any) => item.name.toLowerCase().includes(filters.nameContains!.toLowerCase()) ); } if (filters.templateIn && filters.templateIn.length > 0) { items = items.filter((item: any) => filters.templateIn!.includes(item.templateName)); } if (filters.hasChildrenFilter !== undefined) { // Note: ContentSearchResult does NOT have hasChildren field console.warn('hasChildrenFilter is not supported by ContentSearchResult, filter ignored'); } if (filters.hasLayoutFilter !== undefined) { // Note: ContentSearchResult does NOT have fields array console.warn('hasLayoutFilter is not supported by ContentSearchResult, filter ignored'); } } // Apply client-side sorting if (orderBy && orderBy.length > 0) { items.sort((a: any, b: any) => { for (const sort of orderBy) { const aVal = a[sort.field] || ''; const bVal = b[sort.field] || ''; const comparison = aVal.localeCompare(bVal, undefined, { sensitivity: 'base' }); if (comparison !== 0) { return sort.direction === 'ASC' ? comparison : -comparison; } } return 0; }); } const mappedItems = items.map((item: any) => { // ContentSearchResult fields: id, name, path, templateName, uri, language (String) // Map to SitecoreItem interface return { id: item.id, name: item.name, displayName: item.name, // ContentSearchResult doesn't have displayName path: item.path, templateId: '', // ContentSearchResult doesn't provide templateId templateName: item.templateName, language: item.language || language, // language is String, not object! version: 1, // ContentSearchResult doesn't provide version hasChildren: false, // ContentSearchResult doesn't have hasChildren fields: {}, // ContentSearchResult doesn't have fields array }; }); return { items: mappedItems, pageInfo, totalCount, }; } /** * Get a specific field value from an item * NEW: Supports version parameter */ async getFieldValue( path: string, fieldName: string, language: string = 'en', _database: string = 'master', version?: number ): Promise<{ fieldName: string; value: any; type: string }> { const query = ` query GetField($path: String!, $fieldName: String!, $language: String!, $version: Int) { item(path: $path, language: $language, version: $version) { field(name: $fieldName) { name value } } } `; const result = await this.executeGraphQL(query, { path, fieldName, language, version }); if (!result.item || !result.item.field) { throw new Error(`Field '${fieldName}' not found in item: ${path}`); } return { fieldName: result.item.field.name, value: result.item.field.value, type: 'text', // GraphQL geeft geen type terug, default naar text }; } /** * Get template information */ async getTemplate(templatePath: string, _database: string = 'master'): Promise<any> { // Apply smart language default (templates always 'en') const language = 'en'; const query = ` query GetTemplate($path: String!, $language: String!) { item(path: $path, language: $language) { id name path template { id name } fields(ownFields: false) { name value } } } `; const result = await this.executeGraphQL(query, { path: templatePath, language }); if (!result.item) { throw new Error( `Template not found: ${templatePath} (language: ${language}). Template items must always be queried with language='en'.` ); } return { id: result.item.id, name: result.item.name, path: result.item.path, fields: result.item.fields || [], baseTemplates: [], // GraphQL basic query geeft geen base templates terug }; } /** * Get all fields with values for an item based on its template * NEW FEATURE: Template-based field discovery * * When asked for "fields of item X": * 1. Get the item to find its template * 2. Query all fields from template definition * 3. Get actual values for those fields * 4. Return complete field list with values * * HELIX AWARENESS: * - Supports inherited fields from base templates * - Handles Foundation/Feature/Project template layers */ async getItemFieldsFromTemplate( path: string, language?: string, version?: number ): Promise<Array<{ name: string; value: any; type?: string }>> { // Apply smart language default const effectiveLanguage = this.getSmartLanguageDefault(path, language); // Query to get item with ALL its fields const query = ` query GetItemFields($path: String!, $language: String!, $version: Int) { item(path: $path, language: $language, version: $version) { id name path template { id name } fields(ownFields: false) { name value } } } `; const result = await this.executeGraphQL(query, { path, language: effectiveLanguage, version, }); if (!result.item) { throw new Error(`Item not found: ${path}`); } // Return all fields with their values // ownFields: false includes inherited fields (Helix base templates) return (result.item.fields || []).map((field: any) => ({ name: field.name, value: field.value, type: 'text', // GraphQL doesn't return field type, could be enhanced })); } /** * Get layout/presentation information for an item * Schema requires: site (string), routePath (string), language (string) */ async getLayout(site: string, routePath: string, language: string = 'en'): Promise<any> { const query = ` query GetLayout($site: String!, $routePath: String!, $language: String!) { layout(site: $site, routePath: $routePath, language: $language) { item { id name path displayName } placeholders { name path } } } `; const variables = { site, routePath, language }; const result = await this.executeGraphQL(query, variables); if (!result.layout) { throw new Error(`Layout not found for site: ${site}, route: ${routePath}`); } return result.layout; } /** * Get site configuration information * NEW for /items/master: Uses plural 'sites' query with filtering */ async getSites(name?: string, current?: boolean, includeSystemSites?: boolean): Promise<any[]> { const query = ` query GetSites($name: String, $current: Boolean, $includeSystemSites: Boolean) { sites(name: $name, current: $current, includeSystemSites: $includeSystemSites) { name hostName rootPath startItem language database } } `; const result = await this.executeGraphQL(query, { name, current, includeSystemSites }); if (!result.sites) { throw new Error(`Sites information not available`); } return result.sites; } /** * Get templates information * SCHEMA-VALIDATED: Query.templates exists and returns [ItemTemplate] * ItemTemplate fields: id, name, baseTemplates, fields, ownFields (NO path!) */ async getTemplates(_path?: string): Promise<any[]> { // The templates() query EXISTS in GraphQL schema (Query.templates: [ItemTemplate]) // But ItemTemplate only has: id, name, baseTemplates, fields, ownFields (NO path!) const query = ` query GetTemplates { templates { id name baseTemplates { id name } fields { name type } } } `; const result = await this.executeGraphQL(query, {}); if (!result.templates) { throw new Error( `Templates query failed. Note: ItemTemplate type only has id, name, baseTemplates, fields, ownFields (no path field).` ); } // Return templates // Note: ItemTemplate does NOT have 'path' field per schema! return result.templates; } /** * Create a new Sitecore item * NEW for /items/master: Mutation support! */ async createItem( name: string, template: string, parent: string, language: string = 'en', _fields?: Record<string, any> ): Promise<any> { const mutation = ` mutation CreateItem( $name: String! $template: String! $parent: String! $language: String ) { createItem( name: $name template: $template parent: $parent language: $language ) { id name path displayName } } `; const result = await this.executeGraphQL(mutation, { name, template, parent, language, }); if (!result.createItem) { throw new Error(`Failed to create item: ${name}`); } return result.createItem; } /** * Update an existing Sitecore item * NEW for /items/master: Mutation support! */ async updateItem( path: string, language: string = 'en', version?: number, name?: string, _fields?: Record<string, any> ): Promise<any> { const mutation = ` mutation UpdateItem( $path: String! $language: String $version: Int $name: String ) { updateItem( path: $path language: $language version: $version name: $name ) { id name path displayName } } `; const result = await this.executeGraphQL(mutation, { path, language, version, name, }); if (!result.updateItem) { throw new Error(`Failed to update item: ${path}`); } return result.updateItem; } /** * Delete a Sitecore item * NEW for /items/master: Mutation support! */ async deleteItem(path: string, deletePermanently: boolean = false): Promise<boolean> { const mutation = ` mutation DeleteItem($path: String!, $deletePermanently: Boolean) { deleteItem(path: $path, deletePermanently: $deletePermanently) } `; const result = await this.executeGraphQL(mutation, { path, deletePermanently, }); return result.deleteItem === true; } /** * Scan GraphQL schema via introspection */ async scanSchema(): Promise<any> { const introspectionQuery = ` query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { kind name description fields(includeDeprecated: true) { name description args { name description type { kind name ofType { kind name ofType { kind name } } } } type { kind name ofType { kind name ofType { kind name } } } } inputFields { name description type { kind name ofType { kind name } } } interfaces { name } enumValues(includeDeprecated: true) { name description } possibleTypes { name } } } } `; const result = await this.executeGraphQL(introspectionQuery, {}); return result.__schema; } /** * Analyze schema and extract useful information */ async analyzeSchema(): Promise<any> { const schema = await this.scanSchema(); const analysis: any = { timestamp: new Date().toISOString(), endpoint: this.graphqlEndpoint, queryType: schema.queryType?.name || null, mutationType: schema.mutationType?.name || null, subscriptionType: schema.subscriptionType?.name || null, operations: { queries: [], mutations: [], subscriptions: [], }, customTypes: [], templateTypes: [], inputTypes: [], }; // Process all types for (const type of schema.types) { // Skip introspection types if (type.name.startsWith('__')) continue; // Query operations if (type.name === schema.queryType?.name && type.fields) { for (const field of type.fields) { analysis.operations.queries.push({ name: field.name, description: field.description, arguments: field.args.map((arg: any) => ({ name: arg.name, description: arg.description, type: this.formatType(arg.type), required: arg.type.kind === 'NON_NULL', })), returnType: this.formatType(field.type), }); } } // Mutation operations if (type.name === schema.mutationType?.name && type.fields) { for (const field of type.fields) { analysis.operations.mutations.push({ name: field.name, description: field.description, arguments: field.args.map((arg: any) => ({ name: arg.name, description: arg.description, type: this.formatType(arg.type), required: arg.type.kind === 'NON_NULL', })), returnType: this.formatType(field.type), }); } } // Template types (start with _) if (type.name.startsWith('_') && type.kind === 'OBJECT') { analysis.templateTypes.push({ name: type.name, description: type.description, fields: type.fields?.map((f: any) => f.name) || [], }); } // Input types if (type.kind === 'INPUT_OBJECT') { analysis.inputTypes.push({ name: type.name, description: type.description, fields: type.inputFields?.map((f: any) => ({ name: f.name, type: this.formatType(f.type), })) || [], }); } // Other custom types if ( type.kind === 'OBJECT' && !type.name.startsWith('_') && type.name !== schema.queryType?.name && type.name !== schema.mutationType?.name && type.name !== schema.subscriptionType?.name ) { analysis.customTypes.push({ name: type.name, description: type.description, kind: type.kind, fieldCount: type.fields?.length || 0, }); } } return analysis; } /** * Format GraphQL type for display */ private formatType(type: any): string { if (!type) return 'Unknown'; if (type.kind === 'NON_NULL') { return this.formatType(type.ofType) + '!'; } if (type.kind === 'LIST') { return '[' + this.formatType(type.ofType) + ']'; } return type.name || 'Unknown'; } /** * Parse natural language /sitecore command * Enhanced with better pattern matching and more commands */ async parseSitecoreCommand(command: string): Promise<any> { const lowerCommand = command.toLowerCase().trim(); // Remove /sitecore prefix if present const cleanCommand = lowerCommand.replace(/^\/sitecore\s+/, ''); // Help command if (cleanCommand === 'help' || cleanCommand === '?' || cleanCommand === '') { return { action: 'help', examples: [ '/sitecore get item /sitecore/content/Home', '/sitecore get /sitecore/content/Home version 2', '/sitecore search articles', '/sitecore search for "home page" in /sitecore/content', '/sitecore children of /sitecore/content', '/sitecore field Title from /sitecore/content/Home', '/sitecore templates', '/sitecore sites', '/sitecore create item MyItem with template Sample/Article under /sitecore/content', '/sitecore update item /sitecore/content/Home name "New Home"', '/sitecore delete item /sitecore/content/OldItem', '/sitecore scan schema', '/sitecore examples', '/sitecore help', ], }; } // Examples command if (cleanCommand === 'examples') { return { action: 'examples', categories: [ { category: 'Basic Item Operations', examples: [ { command: '/sitecore get item /sitecore/content/Home', description: 'Get an item by path', }, { command: '/sitecore get /sitecore/content/Home', description: 'Short syntax for get item', }, { command: '/sitecore /sitecore/content/Home', description: 'Even shorter - just the path', }, { command: '/sitecore children of /sitecore/content', description: 'Get all children', }, { command: '/sitecore field Title from /sitecore/content/Home', description: 'Get specific field value', }, ], }, { category: 'Search Operations', examples: [ { command: '/sitecore search articles', description: 'Simple keyword search' }, { command: '/sitecore search for "home page"', description: 'Search with quotes' }, { command: '/sitecore find items with template Article', description: 'Search by template', }, { command: '/sitecore search articles in /sitecore/content', description: 'Search in specific path', }, ], }, { category: 'Templates & Schema', examples: [ { command: '/sitecore templates', description: 'List all templates' }, { command: '/sitecore scan schema', description: 'Analyze GraphQL schema' }, { command: '/sitecore sites', description: 'List all sites' }, ], }, { category: 'Mutations (requires write permissions)', examples: [ { command: '/sitecore create item MyItem with template {GUID} under /sitecore/content', description: 'Create new item', }, { command: '/sitecore update item /sitecore/content/Home name "New Name"', description: 'Update item name', }, { command: '/sitecore delete item /sitecore/content/OldItem', description: 'Delete item', }, ], }, ], }; } // Scan schema command if (cleanCommand.includes('scan') || cleanCommand.includes('schema')) { const analysis = await this.analyzeSchema(); return { action: 'schema_scan', result: analysis, }; } // List templates command if ( cleanCommand === 'templates' || cleanCommand === 'list templates' || cleanCommand === 'show templates' ) { const templates = await this.getTemplates(); return { action: 'list_templates', result: { count: templates.length, templates: templates.slice(0, 20).map((t: any) => ({ name: t.name, path: t.path, id: t.id, })), note: templates.length > 20 ? `Showing first 20 of ${templates.length} templates. Use the API for full access.` : null, }, }; } // List sites command if ( cleanCommand === 'sites' || cleanCommand === 'list sites' || cleanCommand === 'show sites' ) { try { const sites = await this.getSites(); return { action: 'list_sites', result: { count: sites.length, sites: sites.map((s: any) => ({ name: s.name, hostName: s.hostName, database: s.database, language: s.language, })), }, }; } catch (_error) { return { action: 'list_sites', error: 'Sites query returned no data. This might not be configured in your Sitecore instance.', result: { count: 0, sites: [] }, }; } } // Get item with version support // Patterns: "get item PATH", "get PATH", "PATH", "get item PATH version N", "get PATH version N" const getItemVersionMatch = cleanCommand.match( /(?:get\s+(?:item\s+)?)?([\/\w-]+)\s+version\s+(\d+)/ ); if (getItemVersionMatch) { const path = getItemVersionMatch[1].trim(); const version = parseInt(getItemVersionMatch[2]); const item = await this.getItem(path, 'en', 'master', version); return { action: 'get_item', result: item, note: `Retrieved version ${version} of item`, }; } // Get item (various patterns) const getItemMatch = cleanCommand.match( /^(?:get\s+(?:item\s+)?)?([\/].+?)(?:\s+(?:from|in)\s+.+)?$/ ); if (getItemMatch && !cleanCommand.includes('search') && !cleanCommand.includes('field')) { const path = getItemMatch[1].trim(); const item = await this.getItem(path); return { action: 'get_item', result: item, }; } // Search with template filter // Pattern: "find items with template TEMPLATE", "search items with template TEMPLATE" const searchTemplateMatch = cleanCommand.match( /(?:find|search)\s+(?:items\s+)?(?:with|by)\s+template\s+(.+)/ ); if (searchTemplateMatch) { const templateName = searchTemplateMatch[1].trim(); const results = await this.searchItems(undefined, undefined, templateName); return { action: 'search', result: results, note: `Found items with template: ${templateName}`, }; } // Search with path context // Pattern: "search KEYWORD in PATH", "search for KEYWORD in PATH" const searchInPathMatch = cleanCommand.match( /search\s+(?:for\s+)?["\']?(.+?)["\']?\s+in\s+([\/].+)/ ); if (searchInPathMatch) { const searchText = searchInPathMatch[1].trim(); const rootPath = searchInPathMatch[2].trim(); const results = await this.searchItems(searchText, rootPath); return { action: 'search', result: results, note: `Searching for "${searchText}" in ${rootPath}`, }; } // Simple search command // Pattern: "search KEYWORD", "search for KEYWORD", "find KEYWORD" const searchMatch = cleanCommand.match(/(?:search|find)\s+(?:for\s+)?["\']?(.+?)["\']?$/); if (searchMatch) { const searchText = searchMatch[1].trim(); const results = await this.searchItems(searchText); return { action: 'search', result: results, }; } // Children command (various patterns) // Pattern: "children of PATH", "children PATH", "list children of PATH" const childrenMatch = cleanCommand.match(/(?:list\s+)?children\s+(?:of\s+)?([\/].+)/); if (childrenMatch) { const path = childrenMatch[1].trim(); const children = await this.getChildren(path); return { action: 'get_children', result: children, }; } // Field command (various patterns) // Pattern: "field FIELD from PATH", "get field FIELD from PATH", "show field FIELD from PATH" const fieldMatch = cleanCommand.match( /(?:get\s+|show\s+)?field\s+(\w+)\s+(?:from|of|in)\s+([\/].+)/ ); if (fieldMatch) { const fieldName = fieldMatch[1].trim(); const path = fieldMatch[2].trim(); const field = await this.getFieldValue(path, fieldName); return { action: 'get_field', result: field, }; } // Create item command // Pattern: "create item NAME with template TEMPLATE under PARENT" const createMatch = cleanCommand.match( /create\s+(?:item\s+)?(\w+)\s+(?:with\s+)?template\s+([^\s]+)\s+(?:under|in|at)\s+([\/].+)/ ); if (createMatch) { const name = createMatch[1].trim(); const template = createMatch[2].trim(); const parent = createMatch[3].trim(); try { const result = await this.createItem(name, template, parent); return { action: 'create_item', result: result, note: `Created item "${name}" at ${result.path}`, }; } catch (error: any) { return { action: 'create_item', error: error.message, note: 'Item creation failed. This requires write permissions on the API key.', }; } } // Update item command // Pattern: "update item PATH name NAME", "update PATH name NAME" const updateMatch = cleanCommand.match( /update\s+(?:item\s+)?([\/][^\s]+)\s+name\s+["\']?(.+?)["\']?$/ ); if (updateMatch) { const path = updateMatch[1].trim(); const newName = updateMatch[2].trim(); try { const result = await this.updateItem(path, 'en', undefined, newName); return { action: 'update_item', result: result, note: `Updated item name to "${newName}"`, }; } catch (error: any) { return { action: 'update_item', error: error.message, note: 'Item update failed. This requires write permissions on the API key.', }; } } // Delete item command // Pattern: "delete item PATH", "delete PATH", "remove item PATH" const deleteMatch = cleanCommand.match(/(?:delete|remove)\s+(?:item\s+)?([\/].+)/); if (deleteMatch) { const path = deleteMatch[1].trim(); try { const result = await this.deleteItem(path); return { action: 'delete_item', result: { success: result, path: path }, note: result ? `Item deleted: ${path}` : `Item deletion failed: ${path}`, }; } catch (error: any) { return { action: 'delete_item', error: error.message, note: 'Item deletion failed. This requires write permissions on the API key.', }; } } // Unknown command - provide smart suggestions return { action: 'unknown', message: 'Unknown command. Type "/sitecore help" or "/sitecore examples" for available commands.', input: command, suggestions: [ 'Try: /sitecore get item /sitecore/content/Home', 'Try: /sitecore search articles', 'Try: /sitecore children of /sitecore/content', 'Try: /sitecore help', ], }; } /** * Get parent item * NEW FEATURE: Navigate up the item tree */ async getParent( path: string, language: string = 'en', version?: number ): Promise<SitecoreItem | null> { const query = ` query GetParent($path: String!, $language: String!, $version: Int) { item(path: $path, language: $language, version: $version) { parent { id name displayName path template { id name } hasChildren } } } `; const result = await this.executeGraphQL(query, { path, language, version }); if (!result.item || !result.item.parent) { return null; // Root item has no parent } const parent = result.item.parent; return { id: parent.id, name: parent.name, displayName: parent.displayName, path: parent.path, templateId: parent.template.id, templateName: parent.template.name, language: language, version: version || 1, hasChildren: parent.hasChildren, fields: {}, }; } /** * Get all ancestors (parent, grandparent, etc.) up to root * NEW FEATURE: Complete path traversal */ async getAncestors( path: string, language: string = 'en', version?: number ): Promise<SitecoreItem[]> { const ancestors: SitecoreItem[] = []; let currentPath = path; while (true) { const parent = await this.getParent(currentPath, language, version); if (!parent) break; // Reached root ancestors.push(parent); currentPath = parent.path; // Safety check to prevent infinite loops if (ancestors.length > 50) { throw new Error('Too many ancestors (max 50). Possible circular reference.'); } } return ancestors; } /** * Get all versions of an item * NEW FEATURE: Version history */ async getItemVersions( path: string, language: string = 'en' ): Promise<Array<{ version: number; item: SitecoreItem }>> { // Note: GraphQL doesn't have a direct "all versions" query // We need to query version by version until we get null const versions: Array<{ version: number; item: SitecoreItem }> = []; for (let v = 1; v <= 20; v++) { // Check up to 20 versions try { const item = await this.getItem(path, language, 'master', v); versions.push({ version: v, item }); } catch (_error) { // Version doesn't exist, stop searching break; } } return versions; } /** * Get item with statistics (created/updated dates) * NEW FEATURE: Uses inline fragment for Statistics type * NOTE: created/updated are DateField objects with { value } property */ async getItemWithStatistics( path: string, language: string = 'en', version?: number ): Promise< SitecoreItem & { created?: string; updated?: string; createdBy?: string; updatedBy?: string } > { const query = ` query GetItemWithStats($path: String!, $language: String!, $version: Int) { item(path: $path, language: $language, version: $version) { id name displayName path template { id name } hasChildren ... on Statistics { created { value } updated { value } createdBy { value } updatedBy { value } } } } `; const result = await this.executeGraphQL(query, { path, language, version }); if (!result.item) { throw new Error(`Item not found: ${path}`); } const item = result.item; return { id: item.id, name: item.name, displayName: item.displayName, path: item.path, templateId: item.template.id, templateName: item.template.name, language: language, version: version || 1, hasChildren: item.hasChildren, fields: {}, created: item.created?.value, updated: item.updated?.value, createdBy: item.createdBy?.value, updatedBy: item.updatedBy?.value, }; } /** * COMPREHENSIVE DISCOVERY: Get item with ALL dependencies * * NEW FEATURE v1.6.0! * * Automatically discovers and returns: * 1. Content item details * 2. Template with full inheritance chain * 3. All fields from template hierarchy * 4. Renderings associated with template or item * 5. GraphQL resolvers for those renderings * * This enables AI-assisted editing with complete context of what belongs to a content item. * * WORKFLOW: * - Get content item (nl-NL or specified language) * - Extract template ID * - Get template definition (ALWAYS 'en' language) * - Follow template inheritance (base templates recursively) * - Get all fields from template hierarchy * - Search for renderings (if enabled) * - Find resolvers (if enabled) * * @param path - Content item path or ID * @param language - Content language (default: nl-NL) * @param includeRenderings - Include renderings (default: true) * @param includeResolvers - Include resolvers (default: true) */ async discoverItemDependencies( path: string, language: string = 'nl-NL', includeRenderings: boolean = false, includeResolvers: boolean = false, progressCallback?: (step: number, message: string) => void ): Promise<{ item: SitecoreItem; template: any; templateInheritance: any[]; fields: any[]; renderings?: any[]; resolvers?: any[]; summary: { itemName: string; itemPath: string; itemLanguage: string; templateName: string; templatePath: string; totalFields: number; inheritanceDepth: number; renderingCount?: number; resolverCount?: number; }; }> { try { progressCallback?.(1, 'Getting content item...'); console.log(`[Discovery] Step 1: Getting content item...`); // STEP 1: Get content item (in specified language) const item = await this.getItem(path, language); console.log(`[Discovery] Step 1 complete: ${item?.name}`); progressCallback?.(1, `Content item retrieved: ${item?.name}`); if (!item) { throw new Error(`Item not found: ${path} (language: ${language})`); } progressCallback?.(2, 'Getting template definition...'); console.log(`[Discovery] Step 2: Getting template definition...`); // STEP 2: Get template definition (ALWAYS 'en' for templates) // SOLUTION: Template items ARE regular items, just query them in 'en' language // CRITICAL: Format GUID with dashes - GraphQL returns without dashes but requires them in queries const templateId = this.formatGuid(item.templateId); const _templateName = item.templateName; let template: any = null; const templateInheritance: any[] = []; try { // Templates are just items in 'en' language // Use getItem() which works for all items including templates const templateItem = await this.getItem(templateId, 'en'); if (templateItem) { console.log(`[Discovery] Step 2: Template found: ${templateItem.name}`); progressCallback?.(2, `Template retrieved: ${templateItem.name}`); // Get all template fields const templateFields = await this.getItemFieldsFromTemplate(templateId, 'en'); console.log(`[Discovery] Step 2: Retrieved ${templateFields.length} template fields`); progressCallback?.(2, `Template has ${templateFields.length} fields`); template = { id: templateItem.id, name: templateItem.name, displayName: templateItem.displayName, path: templateItem.path, template: { id: templateItem.templateId, name: templateItem.templateName, }, hasChildren: templateItem.hasChildren, fields: templateFields.map((f) => ({ name: f.name, value: f.value })), }; progressCallback?.(3, 'Checking template inheritance...'); console.log(`[Discovery] Step 3: Checking template inheritance...`); // STEP 3: Follow template inheritance (base templates) // Get base templates from __Base template field const baseTemplatesField = template.fields?.find( (f: any) => f.name === '__Base template' ); if (baseTemplatesField && baseTemplatesField.value) { // Parse base template IDs (pipe-separated) // CRITICAL: Format each GUID properly with dashes const baseTemplateIds = baseTemplatesField.value .split('|') .filter((id: string) => id.trim()) .map((id: string) => this.formatGuid(id)); console.log(`[Discovery] Step 3: Found ${baseTemplateIds.length} base template(s)`); progressCallback?.( 3, `Found ${baseTemplateIds.length} base template(s), retrieving...` ); // Get each base template as a regular item (ALWAYS 'en' language) for (const baseId of baseTemplateIds) { try { const baseTemplateItem = await this.getItem(baseId, 'en'); if (baseTemplateItem) { console.log( `[Discovery] Step 3: Retrieved base template: ${baseTemplateItem.name}` ); } if (baseTemplateItem) { templateInheritance.push({ id: baseTemplateItem.id, name: baseTemplateItem.name, displayName: baseTemplateItem.displayName, path: baseTemplateItem.path, template: { id: baseTemplateItem.templateId, name: baseTemplateItem.templateName, }, }); } } catch (_error) { // Skip if base template not found console.error(`Base template not found: ${baseId}`); } } progressCallback?.(3, `Retrieved ${templateInheritance.length} base template(s)`); } } } catch (_error) { console.error(`Template not found: ${templateId}`); } progressCallback?.(4, 'Getting all item fields...'); console.log(`[Discovery] Step 4: Getting all item fields...`); // STEP 4: Get all fields (from item) const fieldsArray = await this.getItemFieldsFromTemplate(path, language); const totalFields = fieldsArray.length; console.log(`[Discovery] Step 4 complete: ${totalFields} fields`); progressCallback?.(4, `Retrieved ${totalFields} fields`); // STEP 5: Search for renderings (if enabled) let renderings: any[] = []; if (includeRenderings && template) { progressCallback?.(5, 'Searching for renderings (this may take a while)...'); console.log(`[Discovery] Step 5: Searching for renderings (this may take a while)...`); try { // Search for renderings in /sitecore/layout/Renderings // Filter by template name (Feature/Module pattern) const renderingSearch = await this.searchItems( template.name, // searchText '/sitecore/layout/Renderings', // rootPath undefined, // templateName filter 'en', // language 'master', // database 50 // maxItems ); renderings = renderingSearch || []; console.log(`[Discovery] Step 5 complete: Found ${renderings.length} rendering(s)`); progressCallback?.(5, `Found ${renderings.length} rendering(s)`); } catch (error) { console.error('[Discovery] Step 5 error: Error searching for renderings', error); } } else { console.log(`[Discovery] Step 5: Skipped (includeRenderings=false)`); progressCallback?.(5, 'Skipped renderings (disabled)'); } // STEP 6: Find resolvers (if enabled) let resolvers: any[] = []; if (includeResolvers && renderings.length > 0) { progressCallback?.(6, 'Searching for resolvers (this may take a while)...'); console.log(`[Discovery] Step 6: Searching for resolvers (this may take a while)...`); try { // Search for resolvers in /sitecore/system/Modules/Layout Service/Rendering Contents Resolvers const resolverSearch = await this.searchItems( template?.name || '', // searchText '/sitecore/system/Modules/Layout Service/Rendering Contents Resolvers', // rootPath undefined, // templateName filter 'en', // language 'master', // database 50 // maxItems ); resolvers = resolverSearch || []; console.log(`[Discovery] Step 6 complete: Found ${resolvers.length} resolver(s)`); progressCallback?.(6, `Found ${resolvers.length} resolver(s)`); } catch (error) { console.error('[Discovery] Step 6 error: Error searching for resolvers', error); } } else { console.log(`[Discovery] Step 6: Skipped (includeResolvers=false or no renderings)`); progressCallback?.(6, 'Skipped resolvers (disabled or no renderings)'); } progressCallback?.(7, 'Building summary...'); console.log(`[Discovery] Step 7: Building summary...`); // STEP 7: Build summary const summary = { itemName: item.name, itemPath: item.path, itemLanguage: language, templateName: template?.name || 'Unknown', templatePath: template?.path || 'Unknown', totalFields: totalFields, inheritanceDepth: templateInheritance.length, ...(includeRenderings && { renderingCount: renderings.length }), ...(includeResolvers && { resolverCount: resolvers.length }), }; console.log(`[Discovery] Complete! Summary:`, summary); progressCallback?.(7, 'Discovery complete!'); return { item, template, templateInheritance, fields: fieldsArray, ...(includeRenderings && { renderings }), ...(includeResolvers && { resolvers }), summary, }; } catch (error) { console.error('[Discovery] Error:', error); throw new Error( `Error discovering item dependencies: ${error instanceof Error ? error.message : String(error)}` ); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GaryWenneker/SitecoreMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server