A Model Context Protocol (MCP) server for Optimizely CMS, providing AI assistants with comprehensive access to Optimizely's GraphQL API and Content Management API.
Current Version: 2.0.0-beta Status: Beta / Pre-Release
This is an active development version and is not yet a release candidate. Features are subject to change, and additional testing is required before production use.
- Discovery-First Architecture: Zero hardcoded assumptions about content types or fields
- Dynamic Schema Introspection: Discovers available content types and fields at runtime
- Unified Content Retrieval: Get any content by URL, key, GUID, or search term in one call
- Visual Builder Support: Full support for Optimizely Visual Builder pages with composition structure
- Content Management: Create and manage content via interactive wizard
- Intelligent Field Mapping: Pattern-based field matching with confidence scoring
- GraphQL & CMA Integration: Direct access to both Graph API (read) and Content Management API (write)
- Smart Caching: Built-in caching for improved performance
- Type Safety: Full TypeScript support with runtime validation
- Graph API: Fast content retrieval, search, and discovery
- Content Management API: Content creation, updates, and draft access
- Dual Authentication: Supports both Graph (single key, HMAC) and CMA (OAuth2) authentication
# Clone the repository
git clone https://github.com/your-org/optimizely-mcp-server.git
cd optimizely-mcp-server
# Install dependencies
npm install
# Build the project
npm run buildCreate a .env file in the project root:
# Server Configuration
SERVER_NAME=optimizely-mcp-server
SERVER_VERSION=1.0.0
TRANSPORT=stdio
# Optimizely Graph Configuration
GRAPH_ENDPOINT=https://cg.optimizely.com/content/v2
GRAPH_AUTH_METHOD=single_key # Options: single_key, hmac, basic, bearer, oidc
GRAPH_SINGLE_KEY=your-single-key
# For HMAC auth:
# GRAPH_APP_KEY=your-app-key
# GRAPH_SECRET_KEY=your-secret-key
# Content Management API Configuration
CMA_BASE_URL=https://api.cms.optimizely.com/preview3
CMA_CLIENT_ID=your-client-id # Get from Settings > API Keys in CMS
CMA_CLIENT_SECRET=your-client-secret
CMA_GRANT_TYPE=client_credentials
CMA_TOKEN_ENDPOINT=https://api.cms.optimizely.com/oauth/token
CMA_IMPERSONATE_USER= # Optional: User email to impersonate (see Impersonation section)
# Optional Configuration
CACHE_TTL=300000 # Cache TTL in milliseconds (default: 5 minutes)
LOG_LEVEL=info # Options: debug, info, warn, error
MAX_RETRIES=3
TIMEOUT=30000# Run with hot reloading
npm run dev
# Run with debug logging
LOG_LEVEL=debug npm run dev# Build and run
npm run build
npm start
# Or run directly
node dist/index.js# Run all unit tests
npm test
# Run tests with coverage
npm run test:coverage
# Type checking
npm run typecheck
# Linting
npm run lintMCP servers communicate via stdio (standard input/output), not HTTP ports:
- No port required - The server doesn't listen on any network port
- Process-based - Claude Desktop spawns your server as a child process
- JSON-RPC messages - Communication happens through stdin/stdout pipes
- Secure - No network exposure, runs only when Claude needs it
Open the configuration file in a text editor:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
For Windows, you can open it quickly with:
notepad %APPDATA%\Claude\claude_desktop_config.json{
"mcpServers": {
"optimizely": {
"command": "node",
"args": ["%USERPROFILE%\\path\\to\\optimizely-mcp-server\\dist\\index.js"],
"env": {
"LOG_LEVEL": "error",
"GRAPH_ENDPOINT": "https://cg.optimizely.com/content/v2",
"GRAPH_AUTH_METHOD": "single_key",
"GRAPH_SINGLE_KEY": "your-key",
"CMA_BASE_URL": "https://api.cms.optimizely.com/preview3/experimental",
"CMA_CLIENT_ID": "your-client-id",
"CMA_CLIENT_SECRET": "your-client-secret",
"CMA_GRANT_TYPE": "client_credentials",
"CMA_TOKEN_ENDPOINT": "https://api.cms.optimizely.com/oauth/token",
"CMA_IMPERSONATE_USER": ""
}
}
}
}In JSON on Windows, you must use double backslashes (\). If your folder path has spaces, this still works because each argument is a separate JSON string.
- Windows: %USERPROFILE% expands to your home directory (e.g., C:\Users\Alice). If Claude doesn’t expand it automatically, replace it with your actual path (e.g., C:\Users\Alice\path\to\optimizely-mcp-server\dist\index.js). In PowerShell, the equivalent is $env:USERPROFILE, but inside this JSON config you should keep %USERPROFILE% or use the full path.
- macOS/Linux: the equivalent shortcut is ~ or $HOME (e.g., /Users/alice or /home/alice). If ~/$HOME isn’t expanded correctly, replace it with the full path.
After saving the config file:
- Completely quit Claude Desktop (not just close the window)
- Start Claude Desktop again
- The Optimizely tools should now be available
In a new Claude conversation, try:
- "Can you list the available Optimizely tools?"
- "Use the health-check tool to test the connection"
If the server doesn't load:
- Check the file path is correct and uses proper escaping (
\\for Windows) - Ensure you've built the project (
npm run build) - Verify the
dist/index.jsfile exists - Check Claude's logs for errors
For other MCP-compatible clients, use the stdio transport configuration:
{
"name": "optimizely",
"transport": {
"type": "stdio",
"command": "node",
"args": ["/path/to/optimizely-mcp-server/dist/index.js"]
},
"env": {
// Environment variables as above
}
}These tools use the Graph API to dynamically discover your CMS structure and retrieve content without hardcoded assumptions.
-
help- 🚀 START HERE! Get context-aware help and learn the discovery-first workflow- Examples:
help({}),help({"topic": "workflow"})
- Examples:
-
get- 🎯 UNIFIED TOOL - Get content by ANY identifier in ONE call- Replaces the old
search→locate→retrieveworkflow - Auto-discovers fields and returns complete content
- ✅ Supports Visual Builder pages with full composition structure
- Examples:
get({"identifier": "/"}),get({"identifier": "Article 4"})
- Replaces the old
-
discover- Find content types and fields dynamically- No hardcoded assumptions about your CMS structure
- Examples:
discover({"target": "types"}),discover({"target": "fields", "contentType": "ArticlePage"})
-
analyze- Deep analysis of content type requirements- Understand fields, constraints, and defaults
- Example:
analyze({"contentType": "ArticlePage"})
-
search- Intelligent content search with auto-discovery⚠️ Note:getis usually better for most use cases- Example:
search({"query": "mcp", "contentTypes": ["ArticlePage"]})
-
locate- Find specific content by ID, key, or path⚠️ Note:getis usually better for most use cases- Example:
locate({"identifier": "/news/article-1"})
-
retrieve- Get full content from Content Management API⚠️ Note:getis usually better (uses faster Graph API)- Use only when
getsuggests it or you need CMA-specific data - Example:
retrieve({"identifier": "12345"})
health-check- Check API connectivity and server healthget-config- Get current server configuration (sanitized)get-documentation- Get documentation for available tools by category
These tools use the Content Management API for write operations and detailed content access:
-
content_creation_wizard- Interactive content creation with discovery- Essential for creating new content
- Example:
content_creation_wizard({"step": "start"})
-
content-test-api- Test CMA connectivity and endpoints- Validates authentication and permissions
- Example:
content-test-api({})
Note: The retrieve tool (listed in Core Tools above) also uses CMA for accessing draft content and version history.
These Graph API discovery tools are duplicates of the new discover tool and will be removed in a future version:
graph-introspection- Usediscoverinsteadtype-discover- Usediscover({"target": "types"})insteadtype-match- Usediscoverinsteadcontent_type_analyzer- Useanalyzeinsteadgraph_discover_types- Usediscover({"target": "types"})insteadgraph_discover_fields- Usediscover({"target": "fields"})insteadgraph-query- Usegetorsearchinstead
Unlike traditional integrations that hardcode content types and field names, this MCP server:
- Never hardcodes content types - No assumptions about "ArticlePage", "StandardPage", etc.
- Never hardcodes field mappings - No predefined paths like "SeoSettings.MetaTitle"
- Discovers everything dynamically - Uses introspection to understand your CMS
- Adapts to any CMS configuration - Works with custom content types and fields
The server uses pattern matching and similarity scoring to:
- Map user-friendly field names to actual CMS fields
- Handle nested properties automatically
- Generate appropriate default values based on field types
- Provide confidence scores for mappings
1. get({"identifier": "homepage"}) # That's it! One call gets everything.
The get tool automatically:
- Detects identifier type (search term, URL, key, or GUID)
- Finds the content
- Discovers all available fields
- Returns complete content including Visual Builder composition
1. help({}) # Learn the workflow
2. discover({"target": "types"}) # Find content types
3. discover({"target": "fields", "contentType": "..."}) # Get fields
4. get({"identifier": "..."}) # Retrieve content
1. discover({"target": "types"}) # Find available types
2. analyze({"contentType": "ArticlePage"}) # Understand requirements
3. content_creation_wizard({...}) # Create with guidance
The get tool fully supports Optimizely Visual Builder (formerly known as Visual Experience Composer) pages:
- ✅ Automatic Detection - Recognizes Visual Builder pages by interface (
_IExperience) - ✅ Complete Composition Retrieval - Returns full structure in a single call
- ✅ Nested Structure - Handles grids, rows, columns, and components
- ✅ Component Content - Includes inline component data directly in composition
- ✅ Recursive Depth - Supports any level of nesting
Visual Builder components come in two types:
- Key:
nullor not present - Content Location: Stored directly in the composition structure
- Access: Content is already included in the
getresponse - Example: Text components with content like "Welcome to our site"
{
"component": {
"_metadata": {
"types": ["Text", "_Component"],
"key": null // ← NULL = inline
},
"Content": "Welcome Text" // ← Content is here
}
}Important: Do NOT try to retrieve inline components separately - the content is already provided!
- Key: Valid GUID (e.g., "f7e7f5c9-1e77-4884-a8fc-a9c9ae56560c")
- Content Location: Stored as separate content items in CMS
- Access: Use
get({"identifier": "component-key"})to retrieve full details - Example: Shared components like Site Settings, reusable blocks
{
"component": {
"_metadata": {
"types": ["ArticleList", "_Component"],
"key": "f7e7f5c91e774884a8fca9c9ae56560c" // ← Has key
}
// May include basic fields, use get() for full content
}
}When working with Visual Builder pages:
- First, retrieve the page with
get({"identifier": "/"}) - Inspect the composition structure for components
- For inline components (null key): Content is already in the response ✅
- For referenced components (has key): Use
get({"identifier": "key"})to fetch full details
// Get a Visual Builder homepage
get({"identifier": "/"})
// Returns complete structure with inline content:
{
"content": {
"_metadata": { ... },
"composition": {
"nodes": [
{
"key": "grid-id",
"displayName": "Welcome Section",
"nodes": [
{
"component": {
"_metadata": {
"types": ["Text"],
"key": null // Inline - content included
},
"Content": "Welcome to our site"
}
},
{
"component": {
"_metadata": {
"types": ["ArticleList"],
"key": "f7e7f5c9..." // Referenced - fetch separately
}
}
}
]
}
]
}
}
}- Performance - Large compositions may take longer to retrieve due to nested structure
- Referenced Component Details - Basic metadata only; full content requires separate
get()call - Display Settings - Not included in current implementation (can be added if needed)
After creating new content using the content_creation_wizard or other creation tools:
- Immediate availability in CMA: Content is immediately available via
retrievetool - Graph API indexing delay: Content may take 1-5 minutes to appear in Graph API results
- Tool behavior: The
getandsearchtools use Graph API and will return "not found" for newly created content until indexing completes
Best practice: After creating content, wait a few minutes before attempting to retrieve it with get or search. Alternatively, use the retrieve tool which queries the CMA directly and has no indexing delay.
- Graph API: Only returns published content
- CMA API: Returns both draft and published content
- New content: Created in draft status by default
- To make content searchable via
get/search, it must be published first
optimizely-mcp-server/
├── src/
│ ├── index.ts # Server entry point
│ ├── register.ts # Tool registration
│ ├── config.ts # Configuration management
│ ├── clients/ # API clients
│ │ ├── graph-client.ts
│ │ └── cma-client.ts
│ ├── logic/ # Tool implementations
│ │ ├── utility/
│ │ ├── graph/
│ │ └── content/
│ ├── types/ # TypeScript types
│ └── utils/ # Utilities
├── tests/ # Test files
├── dist/ # Built output
└── package.json
- Create tool implementation in
src/logic/ - Add tool registration in appropriate section
- Add TypeScript types if needed
- Write tests in
tests/ - Update documentation
- Unit tests for all tool implementations
- Integration tests for API clients
- Mock external API calls
- Test error scenarios
- Maintain >80% coverage
Run automated tests with Vitest:
# Run unit tests
npm test
# Run with coverage report
npm run test:coverageUnit tests are located in /tests/ and cover:
- GraphQL client functionality
- CMA client operations
- Health check features
Test your setup with these npm scripts:
# Check credentials are valid
npm run check:credentials
# Test MCP tools
npm run test:tools
# Test with debug output
npm run test:tools:debug
# Test GraphQL connection
npm run debug:graph
# Validate API key format
npm run validate:key-
Authentication Errors
- Verify your API credentials in
.env - For CMA: Create API keys in Settings > API Keys in your Optimizely CMS instance
- Check token expiration for CMA (tokens expire after 5 minutes)
- Ensure correct auth method for Graph
- Verify your API credentials in
-
Connection Issues
- Verify network connectivity
- Check firewall settings
- Confirm API endpoints are accessible
-
Build Errors
- Run
npm installto ensure dependencies - Check Node.js version (>=18 required)
- Clear
dist/and rebuild
- Run
-
403 Forbidden Errors (Content Creation)
- This typically means insufficient permissions
- See the Impersonation section below for a solution
- Verify the user has content creation rights
- Check the target container allows the content type
Enable debug logging for troubleshooting:
LOG_LEVEL=debug npm startTest server connectivity:
# Using the built tool
echo '{"method": "tools/call", "params": {"name": "health_check"}}' | node dist/index.jsIf you encounter 403 Forbidden errors when creating content, you can use user impersonation to execute API calls as a specific user who has the necessary permissions.
Use impersonation when:
- The API client lacks content creation permissions
- You need to test with different user permission levels
- You want actions attributed to a specific user
-
Enable Impersonation in Optimizely CMS:
- Log into Optimizely CMS as an administrator
- Navigate to Settings > API Clients
- Find your API client
- Enable the "Allow impersonation" option
- Save the changes
-
Configure the MCP Server:
# In your .env file CMA_IMPERSONATE_USER=user@example.com
-
Update Claude Desktop Config (if using environment variables):
{ "mcpServers": { "optimizely": { "env": { "CMA_IMPERSONATE_USER": "user@example.com", // ... other settings } } } }
When impersonation is configured:
- Authentication requests use JSON format with
act_asfield - All content operations execute as the impersonated user
- Created content shows the impersonated user as the author
Test that impersonation is working:
# Run the impersonation test script
node scripts/test-impersonation-final.jsThis will create test content and show which user created it.
- Only enable impersonation when necessary
- Use accounts with minimal required permissions
- Regularly review API client permissions
- Monitor API usage logs for unusual activity
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Run
npm testandnpm run typecheck - Submit a pull request
MIT