Content Delivery MCP
Native Model Context Protocol (MCP) server for the Bloomreach Content delivery tier (HST). It lets AI
assistants and MCP clients read your site's content over a standard, open protocol, reusing the
content model, delivery tier, and authorization you already have.Status: incubating (tools and authorization; tuned for reads). Introduced in Bloomreach Content 17.3.
Overview
MCP is an open, vendor-neutral protocol that lets AI assistants (IDE
agents, chatbots, agentic workflows) talk to external systems through one uniform interface. The
Content Delivery MCP server exposes your Bloomreach Content delivery-tier content to any MCP client, so
an assistant can search and retrieve your published content without a custom, per-client integration.
It runs as a named HST pipeline (McpServerPipeline) on an /mcp mount and speaks JSON-RPC 2.0 over
HTTP POST. It is a framework: you write small provider classes that expose tools over your own content,
the same way you build REST/JAX-RS site services today.
This is the delivery tier (published site content), separate from a future authoring/CMS MCP.
When to use it
Use the Content Delivery MCP when you want an AI assistant to read your published content through a
standard protocol instead of a custom, per-client integration. Once your content speaks MCP, any
MCP-capable client can work with it.
-
Your published content becomes available to AI assistants over a standard protocol, with no migration
and no separate AI content store. -
You expose content once and reach every MCP client, including IDE agents, chatbots, and agentic
workflows. -
You choose which tools to expose and over which content, so the surface fits your use case, such as a
support bot or an internal knowledge assistant. -
Access reuses the same delivery tier and authorization as your live site. You can role-gate sensitive
tools, and they stay invisible to callers who lack the role. -
The server is designed for reading content. See On writes for where writes fit.
-
A tool is an annotated Java method on a plain bean. If you can write a JAX-RS site resource, you can
write an MCP tool, and it can return your content beans directly.
The Bloomreach documentation website is itself queryable over MCP, so an assistant can answer questions
from the official docs.
Note: When you query the Bloomreach documentation website using MCP, you are leveraging this module.
Supported capabilities (this iteration)
| Capability | Status |
|---|---|
Tools (tools/list, tools/call) with JSON-schema inputs and behavior annotations | ✅ Supported |
Authorization and role gating (requiredRole, McpCallerContext) | ✅ Supported |
| Transport: JSON-RPC 2.0 over HTTP POST, with CORS | ✅ Supported |
| Returning HST content beans (default content-bean serialization) | ✅ Supported |
| Writes and content mutation | ⚠️ Possible, not recommended at scale (see On writes) |
Prompts (@McpPrompt, prompts/*) | ❌ Planned |
Resources (@McpResource, resources/*) | ❌ Planned |
Supported MCP protocol revisions (newest first): 2025-11-25, 2025-06-18, 2025-03-26. The server
advertises the latest during initialize and negotiates down to the highest revision the client also
supports.
On writes
The Content Delivery MCP is designed for reading content. Tools default to read-only. You can implement
a write tool by setting readOnly = false and performing the change through the standard CMS workflow.
Writes go through the workflow one document at a time, so they suit small, targeted edits. They are not
meant for bulk changes.
For large-scale content changes, use a content import or migration approach built for bulk operations
rather than driving them through MCP.
Build your own MCP server
These steps add an /mcp endpoint to an existing Bloomreach Content project and expose a few read tools
over your content.
Prerequisites
-
An existing Bloomreach Content 17.3+ project (site + delivery webapp).
-
Java 21, Maven 3.9+.
-
The
content-delivery-mcp-*artifacts available in your Maven repositories.
Step 1: Add the dependencies
In your site components module (site/components/pom.xml):
<dependency> <groupId>com.bloomreach.xm</groupId> <artifactId>content-delivery-mcp-server-incubating</artifactId> </dependency> <dependency> <groupId>com.bloomreach.xm</groupId> <artifactId>content-delivery-mcp-api-incubating</artifactId> </dependency>
No <version> is needed. Both artifacts are managed by the Bloomreach Content enterprise release POM,
so a project that inherits it picks up the right version automatically, as with the other platform
add-ons. The api artifact carries the annotations you compile against (@McpTool, @McpParam,
McpCallerContext). The server artifact carries the pipeline, valve, registry, and default mapper.
Step 2: Import the pipeline and register your tool provider
Create site/components/src/main/resources/META-INF/hst-assembly/overrides/custom-mcp.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- Import the stock Content Delivery MCP pipeline (McpServerPipeline, mcpServiceValve, mcpRegistry, default content-bean mcpObjectMapper, CORS valve). --> <import resource="classpath:/com/bloomreach/xm/mcp/SpringComponentManager-mcp-pipeline.xml" /> <!-- Your project's MCP tool provider. --> <bean id="myMcpTools" class="com.example.site.mcp.MyMcpTools" /> <!-- Register it (overrides the module's empty default list). --> <bean id="customMcpToolProviders" class="org.springframework.beans.factory.config.ListFactoryBean"> <property name="sourceList"> <list> <ref bean="myMcpTools" /> </list> </property> </bean> </beans>
Add more <ref> entries to customMcpToolProviders as you add provider classes. Tool names must be
unique across all providers.
Step 3: Write a tool provider
A provider is a plain bean. Each @McpTool method is a tool, and each parameter is described with
@McpParam. Returning a content bean is fine. The default mapper serializes it.
package com.example.site.mcp; import java.util.ArrayList; import java.util.List; import org.hippoecm.hst.container.RequestContextProvider; import org.hippoecm.hst.content.beans.query.HstQuery; import org.hippoecm.hst.content.beans.query.HstQueryResult; import org.hippoecm.hst.content.beans.standard.HippoBean; import org.hippoecm.hst.content.beans.standard.HippoBeanIterator; import org.hippoecm.hst.core.request.HstRequestContext; import com.bloomreach.xm.mcp.annotations.McpParam; import com.bloomreach.xm.mcp.annotations.McpTool; import com.example.site.beans.NewsDocument; public class MyMcpTools { @McpTool(name = "search_news", description = "Search news documents and return compact summaries", readOnly = true) public List<NewsDocument> searchNews( @McpParam(name = "limit", description = "Maximum number of results", required = false) final Integer limit) throws Exception { final HstRequestContext ctx = RequestContextProvider.get(); final HippoBean scope = ctx.getSiteContentBaseBean(); // scope to this channel final HstQuery query = ctx.getQueryManager().createQuery(scope, NewsDocument.class, true); query.setLimit(limit == null ? 10 : Math.min(limit, 50)); query.addOrderByDescending("myproject:date"); final HstQueryResult result = query.execute(); final HippoBeanIterator it = result.getHippoBeans(); final List<NewsDocument> out = new ArrayList<>(); while (it.hasNext()) { out.add((NewsDocument) it.nextHippoBean()); } return out; } }
Notes:
-
You can add
McpCallerContextas a method parameter. The framework injects it, and it is never on
the wire. Use it for custom authorization logic beyondrequiredRole. -
To reuse the standard HST helpers (
getHstQueryManager,getMountContentBaseBean, and the
object/link helpers), extendorg.hippoecm.hst.jaxrs.services.AbstractResource, as a JAX-RS resource
does. You do not need an MCP-specific base class. -
@McpToolcarries behavior hints the client can use:readOnly(defaulttrue),idempotent,
openWorld,destructive(only meaningful on a write tool), andtitle.
Step 4: Add the /mcp mount
In your site's HST host config (repository-data/site/.../hcm-config/hst/hosts.yaml), add an unmapped
child mount under the site root that uses the MCP pipeline:
/mcp: jcr:primaryType: hst:mount hst:ismapped: 'false' hst:namedpipeline: McpServerPipeline hst:types: [rest] hst:responseheaders: ['Access-Control-Allow-Origin: *', 'Access-Control-Allow-Methods: POST, OPTIONS', 'Access-Control-Allow-Headers: Content-Type, MCP-Protocol-Version']
The endpoint is then http(s)://<host>/site/mcp. See CORS for the response
headers.
Step 5: Build and run
mvn clean install && mvn -P cargo.run
Wait for the Tomcat Server startup line.
Step 6: Call it
The endpoint is JSON-RPC 2.0 over HTTP POST. Public tools work anonymously:
BASE=http://localhost:8080/site/mcp # Handshake: the server reports its identity and the negotiated protocol version curl -s -X POST $BASE -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' # List tools curl -s -X POST $BASE -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' # Call a tool curl -s -X POST $BASE -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_news","arguments":{"limit":5}}}'
Step 7 (optional): Authentication and role gating
Gate a tool on a servlet role:
@McpTool(name = "admin_info", description = "Admin-only diagnostics (requires the 'admin' role)", requiredRole = "admin") public SiteInfo adminInfo() { ... }
A caller lacking the role never sees the tool in tools/list, and calling it returns the same
Unknown tool response as a non-existent tool. A gated tool never confirms its own existence.
The project wires authentication as a servlet filter ahead of HstFilter that populates
getUserPrincipal() and isUserInRole(). Use HTTP Basic for a quick start, or OAuth2 or an identity
provider for production. Because gating builds on the standard servlet security APIs, it integrates with
Spring Security: put the Spring Security filter chain ahead of HstFilter, and the valve reads the
authenticated principal and roles with no extra wiring. McpServiceValve then enforces requiredRole
from those APIs.
Step 8 (optional): Connect an MCP client
Most MCP clients accept an HTTP server entry, for example a project-scoped .mcp.json:
{ "mcpServers": { "content-delivery-mcp": { "type": "http", "url": "http://localhost:8080/site/mcp" } } }
Add a second entry with an Authorization header to connect as an authenticated (for example, admin)
caller.
Serialization: returning content beans
A tool can return an HST content bean directly. A plain DTO is not required. The mcpObjectMapper bean
serializes the return value. The default mapper works out of the box; you can replace it for richer
output, or adjust its output with mixins.
Default (out of the box)
DefaultMcpObjectMapperFactory and DefaultMcpHippoBeanModule are wired by default. The default mapper
has no Page Model API dependency, so a tool that returns a bean works without extra setup:
-
framework-declared getters are dropped (recursion- and JCR-safe) via an origin filter, keeping
project getters plus a small identity allow-list (path,name,identifier,localeString); -
rich text (
HippoHtml) renders as its HTML string, and images/resources as a resolvable site link; -
JCR access types (
Session,Item,JCRValueProvider) are ignored, and write-nesting depth is
capped as a cycle backstop.
These representations are basic: HTML is not link-rewritten, and an image is a single link rather than
its variants. The default ensures a tool that returns a bean does not fail serialization.
Recommended for production: Page Model API mapper
For output that matches the Page Model API (link-rewritten HTML, full image and resource variants,
@PageModelIgnore honored), replace the mcpObjectMapper bean with one built on the platform's
PageModelObjectMapperFactory:
public class MyMcpObjectMapperFactory { public ObjectMapper createObjectMapper() { return new PageModelObjectMapperFactory().createPageModelObjectMapper(); } }
<bean id="mcpObjectMapperFactory" class="com.example.site.mcp.MyMcpObjectMapperFactory"/> <bean id="mcpObjectMapper" factory-bean="mcpObjectMapperFactory" factory-method="createObjectMapper"/>
Start with the default, and move to the Page Model API mapper when you need richer output.
Shaping output with mixins
You do not have to change a content bean to change how it serializes. The default mapper merges in
project Jackson mixins from the mcpJacksonObjectMapperExtraMixins bean, a map of target class to mixin
class. Override that bean in your project's hst-assembly to hide or rename fields, or to @JsonIgnore
a getter that should not be exposed (or that misbehaves during serialization), without editing the bean
itself:
<bean id="mcpJacksonObjectMapperExtraMixins" class="org.springframework.beans.factory.config.MapFactoryBean"> <property name="sourceMap"> <map key-type="java.lang.Class" value-type="java.lang.Class"> <entry key="com.example.site.beans.NewsDocument" value="com.example.site.mcp.NewsDocumentMixin"/> </map> </property> </bean>
public abstract class NewsDocumentMixin { @JsonIgnore abstract String getInternalNotes(); // keep this field out of the MCP output }
This is the lightest-weight extensibility point. Use it before replacing the whole mapper.
Security notes
-
Role gating is the authorization model.
@McpTool(requiredRole = "...")hides and blocks a tool for
callers without the role, indistinguishably from a non-existent tool. Caller identity and roles come
fromMcpCallerContext, resolved from the standard servlet principal and role APIs. -
Scope lookups to the channel. A tool that resolves a document by a client-supplied path or UUID runs
on the delivery session. That session can read any published document under/content, including
documents outside the current channel. Confine such lookups to the site content base (reject a bean
whose canonical path is not underctx.getSiteContentBaseBean().getCanonicalPath()). Query-based
tools that scope togetSiteContentBaseBean()are already confined. -
The
xm.*role gotcha. Gating usesHttpServletRequest.isUserInRole, and HST excludes standard
userroles prefixedxm.by default (security.authentication.excluded.userrole.prefixes = xm.). To
gate on anxm.*role, opt it in viasecurity.authentication.included.userrole.prefix. Otherwise
the tool stays hidden even for a caller who holds the role.
CORS (browser clients)
CLI, desktop, and server-side MCP clients do not preflight, so a plain Access-Control-Allow-Origin on
the /mcp mount is enough. A browser client preflights a cross-origin JSON POST and asks for the
Content-Type and MCP-Protocol-Version headers, so the mount must also allow them:
hst:responseheaders: ['Access-Control-Allow-Origin: *', 'Access-Control-Allow-Methods: POST, OPTIONS', 'Access-Control-Allow-Headers: Content-Type, MCP-Protocol-Version']
The CORS valve merges these into the OPTIONS response. Add Authorization to Allow-Headers too if
your browser client authenticates with a bearer or basic header.
Current limitations
Known limitations in this release, with workarounds.
| Limitation | What it means | Workaround |
|---|---|---|
| Tuned for reads | The server suits reading content. Writes are possible (a tool with readOnly = false) but go one document at a time through workflow. | Use it for small, targeted edits. For bulk changes, use a content import or migration approach built for bulk operations. |
| No resources yet | You cannot expose content as addressable, user-pinnable "resources" (resources/*). | Expose the same content as a tool. Resource support is planned. |
| No prompts yet | You cannot ship curated prompt workflows (prompts/*). | Put guidance in tool descriptions. Prompt support is planned. |
| Requires code | It is a framework; you write provider classes. | Follow the build steps above. |
| Basic default serialization | Rich text is not link-rewritten, and an image is a single link rather than its variants. | Use the Page Model API mapper for production-quality output. |
| Project-supplied authentication | The module gates on roles but does not ship an authentication mechanism. | Add a servlet filter ahead of HstFilter (HTTP Basic for demos; OAuth2 or Spring Security for production). |
| Channel scoping for path/UUID lookups | A by-path or by-UUID tool can read outside the current channel if unconfined. | Confine lookups to getSiteContentBaseBean().getCanonicalPath() (see Security notes). |
| Incubating API | Annotations and wiring may change before general availability. | Expect small migration steps between releases. |
| Varying client support | Not every MCP client implements every feature. | Tools are the most broadly supported primitive, which is what this release provides. |
Troubleshooting
-
JsonMappingExceptionwhen a tool returns a content bean. A getter on the bean, or a nested plugin
bean, throws or recurses during serialization. A common cause is a Forge or plugin bean such as
taxonomy'sTaxonomyClassification. The default mapper drops framework-declared getters to avoid
this. If it still occurs, add a Jackson mixin that@JsonIgnores the offending getter, or switch to
the Page Model API mapper. -
A field is missing from the JSON. The origin filter dropped it because its getter is declared in a
framework package. EnableDEBUGlogging oncom.bloomreach.xm.mcp.DefaultMcpHippoBeanModuleto see
which property was dropped and by which declaring class, then add it to the identity allow-list or use
the Page Model API mapper. -
A caller has the role but the gated tool stays hidden. If it is an
xm.*standard userrole, HST
excludes it by default. Include it viasecurity.authentication.included.userrole.prefix. -
A browser MCP client's request is blocked by CORS. The mount allowed the origin but not the requested
headers. AddAccess-Control-Allow-Headers: Content-Type, MCP-Protocol-Version. -
Images and rich text look basic. This is expected with the default mapper. Use the Page Model API
mapper for link-rewritten HTML and full image variants.
Reference
- Protocol: Model Context Protocol, versioning.