<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Mardy Abdelkareem</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/" /><updated>2026-10-14T05:01:48.0000000Z</updated><id>https://world.optimizely.com/blogs/mardy-abdelkareem/</id><generator uri="https://world.optimizely.com/blogs/mardy-abdelkareem/" version="2.0">Optimizely World</generator><entry><title>Computed Fields in CMS 13 Content Contracts with Optimizely Graph</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/computed-fields-in-cms-13-content-contracts-a-practical-guide-with-optimizely-graph-c-sdk/" /><id>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/computed-fields-in-cms-13-content-contracts-a-practical-guide-with-optimizely-graph-c-sdk/</id><published>2026-10-14T05:01:48.0000000Z</published><updated>2026-10-14T05:01:48.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph.&lt;/p&gt;
&lt;p&gt;But what if the shared field doesn't exist as a stored CMS property, and its value needs to be calculated differently depending on the content type?&lt;/p&gt;
&lt;p&gt;For example, imagine a search feature that includes articles, events, and images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Articles use &lt;strong&gt;Heading&lt;/strong&gt; as their search title.&lt;/li&gt;
&lt;li&gt;Events use &lt;strong&gt;EventName&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Images use &lt;strong&gt;ImageTitle&lt;/strong&gt;, falling back to their name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We want all three to expose a common &lt;strong&gt;SearchTitle&lt;/strong&gt; field in Graph, without adding another persisted property to each content type.&lt;/p&gt;
&lt;p&gt;Here's a working approach tested with &lt;strong&gt;Optimizely CMS 13.3.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;1. Define a Content Contract&lt;/h2&gt;
&lt;p&gt;First, define a Content Contract with a method that each implementing content type uses to provide its search title.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ContentType]
public interface ISearchableContent : IContentData
{
    string GetSearchTitle();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each content type implements the same contract but calculates the search title using its own properties. For example, an article page uses &lt;strong&gt;Heading&lt;/strong&gt;, while an event page uses &lt;strong&gt;EventName&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArticlePage : PageData, ISearchableContent
{
    public virtual string Heading { get; set; }

    public string GetSearchTitle() =&amp;gt;
        string.IsNullOrWhiteSpace(Heading) ? Name : Heading;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class EventPage : PageData, ISearchableContent
{
    public virtual string EventName { get; set; }

    public string GetSearchTitle() =&amp;gt;
        string.IsNullOrWhiteSpace(EventName) ? Name : EventName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both content types implement the same contract but calculate &lt;strong&gt;SearchTitle&lt;/strong&gt; differently. The same approach works for media types, such as images, using their own properties.&lt;/p&gt;
&lt;p&gt;Make sure the relevant content types implement or are associated with the Content Contract in CMS so they can be queried together in Graph.&lt;/p&gt;
&lt;p&gt;This keeps the calculation close to each content type and avoids maintaining a central switch statement.&lt;/p&gt;
&lt;h2&gt;2. Expose the Computed Field in Graph&lt;/h2&gt;
&lt;p&gt;Next, create an extension method and register it using Graph indexing conventions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class SearchExtensions
{
    public static string SearchTitle(this ISearchableContent content)
        =&amp;gt; content.GetSearchTitle();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the computed field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddContentGraph(configureConventions: conventions =&amp;gt;
{
    conventions.ForInstancesOf&amp;lt;ISearchableContent&amp;gt;()
        .IncludeField(
            x =&amp;gt; x.SearchTitle(),
            IndexingType.Searchable);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important detail is that &lt;strong&gt;SearchTitle is computed during indexing&lt;/strong&gt;. We don't need to add a persisted SearchTitle property to our CMS models.&lt;/p&gt;
&lt;p&gt;The resulting Graph schema exposes the field through &lt;strong&gt;ISearchableContent&lt;/strong&gt;, regardless of the underlying content type.&lt;/p&gt;
&lt;h2&gt;3. Query It with the Graph C# SDK&lt;/h2&gt;
&lt;p&gt;The interesting part is that the computed field can also be used in strongly typed SDK queries.&lt;/p&gt;
&lt;p&gt;First, define a lightweight result model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SearchResult
{
    public string SearchTitle { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then query the Content Contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = graphClient
    .QueryContent&amp;lt;ISearchableContent&amp;gt;()
    .Where(x =&amp;gt; x.SearchTitle().Contains("Optimizely"))
    .Limit(20)
    .IncludeTotal()
    .Fields&amp;lt;SearchResult&amp;gt;(x =&amp;gt; x.SearchTitle);

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our test, the SDK translated the filtering expression into GraphQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query ISearchableContentQuery {
  ISearchableContent(
    where: { SearchTitle: { contains: "Optimizely" } }
    limit: 20
  ) {
    items {
      SearchTitle
    }
    total(all: true)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query returned one matching event with &lt;strong&gt;SearchTitle&lt;/strong&gt; equal to &lt;strong&gt;Optimizely Developer Webinar&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here's the interesting detail: &lt;strong&gt;SearchTitle()&lt;/strong&gt; is a C# extension method, not a stored CMS property. Graph indexing conventions expose it as a searchable field, and the Graph C# SDK translates the expression into a server-side GraphQL filter rather than filtering an in-memory collection.&lt;/p&gt;
&lt;p&gt;We also verified sorting by the computed field, language-specific values, and recalculation after updating and publishing the underlying content.&lt;/p&gt;
&lt;h2&gt;Key Takeaway&lt;/h2&gt;
&lt;p&gt;By combining &lt;strong&gt;CMS 13 Content Contracts, Graph indexing conventions, and the Graph C# SDK&lt;/strong&gt;, we can expose a shared searchable field across different content types while keeping the calculation specific to each type.&lt;/p&gt;
&lt;p&gt;No additional persisted CMS properties are required.&lt;/p&gt;
&lt;p&gt;Keep computed field calculations lightweight and deterministic, since they run as part of the indexing process.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.optimizely.com/cms-13/docs/indexing-conventions"&gt;Optimizely CMS 13 &amp;mdash; Indexing Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="/link/5ffa0c0d7b184f049358e62d7fcad94f.aspx"&gt;Introducing Optimizely CMS 13 Graph SDK &amp;mdash; Jake Minard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Setting Up an Optimizely CMS 13 MCP Server Locally with Codex</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/" /><id>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/</id><published>2026-10-12T04:41:05.0000000Z</published><updated>2026-10-12T04:41:05.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;Optimizely CMS 13 supports &lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro"&gt;Model Context Protocol (MCP)&lt;/a&gt;, allowing AI tools such as Claude and Codex to interact with CMS content and metadata through an MCP server.&lt;/p&gt;
&lt;p&gt;In this post, I'll show how I configured the MCP server in my local CMS 13 Alloy project, connected it to Codex in Visual Studio Code, and used it to explore content types, create pages, and build a Visual Builder experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Optimizely CMS 13 (I'm using Alloy with CMS 13.3.0).&lt;/li&gt;
&lt;li&gt;An MCP-compatible AI client (I'm using Codex in VS Code).&lt;/li&gt;
&lt;li&gt;A local CMS instance running over HTTPS, for example, https://localhost:5000.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configure the MCP Server&lt;/h2&gt;
&lt;p&gt;Install the &lt;strong&gt;Optimizely.Cms.Opal.Tools&lt;/strong&gt; NuGet package (version 13.2.0 or later):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Optimizely.Cms.Opal.Tools --version 13.2.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the MCP server in &lt;strong&gt;Startup.cs&lt;/strong&gt; using client-secret authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddCmsMcpServer()
    .WithClientSecretAuthentication();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then map the MCP endpoint and require client-secret authorization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.UseEndpoints(endpoints =&amp;gt;
{
    endpoints.MapContent();

    endpoints.MapMcp("/mcp")
        .RequireClientSecretAuthorization();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The MCP endpoint is now available at &lt;strong&gt;https://localhost:5000/mcp&lt;/strong&gt;, or your application's configured HTTPS address.&lt;/p&gt;
&lt;h2&gt;Configure Client-Secret Authentication&lt;/h2&gt;
&lt;p&gt;Make sure a client secret is configured for your local CMS application. We'll use it to authenticate Codex when connecting to the MCP server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Never commit your client secret to source control or expose it in screenshots.&lt;/p&gt;
&lt;h2&gt;Connect Codex in VS Code&lt;/h2&gt;
&lt;p&gt;Open &lt;strong&gt;Codex Settings &amp;rarr; MCP servers &amp;rarr; Add server&lt;/strong&gt; and select &lt;strong&gt;Streamable HTTP&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name:&lt;/strong&gt; optimizely-cms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL:&lt;/strong&gt; https://localhost:5000/mcp&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bearer token env var:&lt;/strong&gt; Leave empty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Header:&lt;/strong&gt; Authorization, with your configured client secret as the value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alternatively, configure the server in Codex's &lt;strong&gt;config.toml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mcp_servers.optimizely-cms]
type = "http"
url = "https://localhost:5000/mcp"

[mcp_servers.optimizely-cms.headers]
Authorization = "your-secret-value"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the placeholder with your actual secret and enable the server in Codex Settings. The secret is passed directly in the Authorization header, without a Bearer prefix.&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;img style="width: 100%; max-width: 500px; height: auto;" src="/link/1b35b3874d3149f7a6065da7c50869d3.aspx" alt="Codex MCP server configuration" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;em&gt;Configuring the local Optimizely MCP server in Codex.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Verify the Integration&lt;/h2&gt;
&lt;p&gt;With the CMS running, open a new Codex chat in VS Code and try:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;List the available content types in my local Optimizely CMS instance using the MCP server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex retrieved the registered content types, including &lt;strong&gt;StandardPage&lt;/strong&gt; and &lt;strong&gt;StandardExperienceData&lt;/strong&gt;, and could inspect their properties, locales, and available Visual Builder components.&lt;/p&gt;
&lt;p&gt;You can also retrieve existing content:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Find the published content items of type ProductTestPage and show their properties.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These read-only operations confirm that Codex can access CMS content and metadata through MCP.&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;img style="width: 100%; max-width: 650px; height: auto;" src="/link/7ff3dbdcef3d48c59233445efb77db39.aspx" alt="Codex discovering Optimizely CMS content types" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;em&gt;Codex retrieving content types from the local CMS instance.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Create Content Using Codex&lt;/h3&gt;
&lt;p&gt;Next, I asked Codex to create an unpublished page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create "MCP Demo Page" under Start using StandardPage. Add a heading and paragraph to MainBody, and save it as an English draft.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex created the page and updated its content through the MCP server.&lt;/p&gt;
&lt;p&gt;I also tested Visual Builder:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create an unpublished "Alloy Demo Experience" using StandardExperienceData, with a BlankSection containing a VisualBuilderTestBlock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex created the experience using the existing CMS content types and Visual Builder components. Both items were saved as drafts without publishing.&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;img style="width: 100%; max-width: 750px; height: auto;" src="/link/058a28868a6d4b519226eaefffb487d1.aspx" alt="MCP Demo Page created in Optimizely CMS" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;em&gt;The MCP Demo Page created through Codex and saved as a draft.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Troubleshooting Content Permissions&lt;/h3&gt;
&lt;p&gt;During content creation, I encountered an authorization error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Access denied: You do not have permission to create content in this location.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The MCP client authenticated successfully with a &lt;strong&gt;CmsAdmins&lt;/strong&gt; role claim, which was mapped to &lt;strong&gt;WebAdmins&lt;/strong&gt; in the application configuration. Although the Start page granted Create permission to WebAdmins, the MCP client was still denied access.&lt;/p&gt;
&lt;p&gt;The issue was resolved by explicitly granting the &lt;strong&gt;MCP Client&lt;/strong&gt; permissions on the Start page through CMS access rights. Afterward, Codex could create and update content.&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;img style="width: 100%; max-width: 750px; height: auto;" src="/link/28ac25ea3076497792a6660ed63434cc.aspx" alt="MCP Client permissions on the Start page" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;em&gt;Granting the MCP Client access on the Start page resolved the create denial.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; For this local demo, all permissions were enabled, including Publish and Administer. In a real environment, grant only the permissions needed for the intended operations.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With Codex connected to Optimizely CMS 13 through MCP, we were able to explore content types, create pages, and work with Visual Builder directly from VS Code.&lt;/p&gt;
&lt;p&gt;One thing worth noting is that connecting successfully doesn't necessarily mean the MCP client has permission to create or update content. You'll still need to configure the appropriate access rights in CMS.&lt;/p&gt;</summary></entry></feed>