Model Context Protocol
In my last post, we discussed how Anthropic’s Model Context Protocol (MCP) is helping to unlock proprietary data sources to AI Agents, and how Tridion Docs’ structured DITA content enriches these tools. This post describes how to build a Java-based MCP Server against Tridion Doc’s Dynamic Experience Delivery (DXD) Content Service, bringing the power of structured DITA content to an Agentic world.
Connecting Tridion DXD to Agentic systems
Architectural Refresher
The Tridion Docs Dynamic Experience Delivery (DXD) platform is a high-performance content delivery framework optimized for structured documentation.
It provides an API-first architecture built on GraphQL endpoints and provides advanced search and retrieval of Tridion Docs Topics. It was built to support customers of all sizes, including those with enterprise documentation needs.
Topics are stored within an OpenSearch cluster, together with a range of metadata that enables end-users to search for topics based upon term-based matches, natural language questions, and existing topic context. It comes with a sophisticated recommendation engine for locating the most relevant content based upon an existing topic. Metadata, such as Product, Version, Audience and Goal form an integral part of the hybrid retrieval process.
MCP overview
Model Context Protocol (MCP) is an open standard that lets AI assistants connect to external data sources and tools in a secure, controlled way. It is a bridge between your AI assistant and the various systems it needs to access to help you get work done.
The protocol solves a common problem: AI assistants are powerful, but they're often isolated from the specific data and tools you need them to work with. MCP changes this by providing a standardized way for assistants to interact with external resources while maintaining security and user control.
MCP organizes these external capabilities into three main categories:
Tools
- Functions the AI can execute on your behalf
- File system operations
- Database queries
- API calls to external services
- Custom business logic execution
Resources
- Data sources the AI can read from
- Files and documents
- Database contents
- Web pages and APIs
- Real-time data streams
Prompts
- Reusable prompt templates
- Context-specific instructions
- Workflow guidance
- Domain-specific knowledge
The key benefit is that MCP creates a standard interface. Instead of every AI assistant needing custom integrations with every possible data source or tool, developers can build MCP servers that any compatible assistant can use. This means better interoperability and less duplicated effort across the ecosystem.
MCP integration technologies
In this example, we’ll be creating some agentic MCP tools against DXD’s GraphQL Content API, using standard Spring libraries where possible, including:
- Spring AI
This project aims to streamline the development of Java applications that incorporate artificial intelligence functionality without unnecessary complexity.
The project draws inspiration from notable Python projects, such as LangChain and LlamaIndex, but Spring AI is not a direct port of those projects. The project was founded with the belief that the next wave of Generative AI applications will not be only for Python developers but will be ubiquitous across many programming languages.
Spring AI MCP provides annotated support for tools, resources, prompts and completions. This example will focus on the tools use case.
This project recently reached General Availabilty - Spring GraphQL
This project provides support for Spring applications built on GraphQL Java. It is a collaboration between the GraphQL Java team and Spring engineering. Their shared philosophy is to provide as little opinion as possible while focusing on comprehensive support for a wide range of use cases.
Spring for GraphQL is the successor of the GraphQL Java Spring project from the GraphQL Java team. It aims to be the foundation for all Spring, GraphQL applications. - Spring Security OAuth2
This library provides a framework for implementing OAuth 2.0 authorization flows in Spring applications, handling the complexity of token-based authentication and authorization between clients, resource servers, and authorization servers.
Project setup
We’ll start by creating a new maven module for this MCP Server:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sdl.delivery</groupId>
<artifactId>tridion-dxd-mcp-server</artifactId>
<version>1.0.0</version>
<name>Tridion DXD MCP Server</name>
<description>An example Model Context Protocol (MCP) Server for Tridion DXD</description>
<scm>
<developerConnection>scm:git:ssh://git@github.com:RWS/tridion-dxd-mcp-server.git</developerConnection>
<url>https://github.com/RWS/tridion-dxd-mcp-server</url>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<mcp-annotations.version>0.1.0</mcp-annotations.version>
<netty.version>4.1.119.Final</netty.version>
<spring.version>6.2.7</spring.version>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-boot.version>3.5.0</spring-boot.version>
<spring-graphql.version>1.4.0</spring-graphql.version>
<build-helper-maven-plugin.version>3.6.0</build-helper-maven-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<maven-enforcer-plugin.version>3.5.0</maven-enforcer-plugin.version>
<graphqlcodegen-maven-plugin.version>3.0.2</graphqlcodegen-maven-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql</artifactId>
<version>${spring-graphql.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.logaritex.mcp</groupId>
<artifactId>mcp-annotations</artifactId>
<version>${mcp-annotations.version}</version>
</dependency>
<dependency>
<groupId>com.logaritex.mcp</groupId>
<artifactId>spring-ai-mcp-annotations</artifactId>
<version>${mcp-annotations.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-resolver-dns-native-macos</artifactId>
<classifier>osx-aarch_64</classifier>
<version>${netty.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.logaritex.mcp</groupId>
<artifactId>spring-ai-mcp-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql</artifactId>
</dependency>
<!-- This native dependency is needed on Mac to support proper DNS resolution -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-resolver-dns-native-macos</artifactId>
<classifier>osx-aarch_64</classifier>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven-enforcer-plugin.version}</version>
<executions>
<execution>
<id>enforce-maven</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[${java.version},22]</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
<!-- This plugin is used to generate datatypes from the schema.graphqls file -->
<plugin>
<groupId>io.github.deweyjose</groupId>
<artifactId>graphqlcodegen-maven-plugin</artifactId>
<version>${graphqlcodegen-maven-plugin.version}</version>
<configuration>
<packageName>com.sdl.delivery.content.graphql.generated</packageName>
<generateClientApi>true</generateClientApi>
<generateInterfaces>false</generateInterfaces>
<generateDataTypes>true</generateDataTypes>
<addGeneratedAnnotation>true</addGeneratedAnnotation>
<subPackageNameDatafetchers>datafetchers</subPackageNameDatafetchers>
<generateIsGetterForPrimitiveBooleanFields>true</generateIsGetterForPrimitiveBooleanFields>
<typeMapping>
<Map>java.util.Map</Map>
</typeMapping>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- This plugin adds the generated sources to the classpath -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Schema generation
We use two build plugins during the build. The first uses the graphqlcodegen-maven-plugin to generate datatypes and client code from the Content Service’s GraphQL Schema. The schema itself can be generated via introspection against a running Content Service using a number of tools. One option is to use the Apollo GraphQL CLI:
npm -g install graphql apollo
apollo client:download-schema --endpoint=http://<your-ip>:8081/cd/api schema.graphqls
Once generated, the updated schema.graphqls file should be placed in the src/main/resources/schema
folder.
The second build plugin copies the generated datatypes into the classpath.
GraphQL Authentication
To be able to interact with the DXD Content Service, it’s necessary to configure an OAuth2 GraphQL Client. This involves creating a standard Spring Security OAuth2 configuration class:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Value("${content.url}")
private String contentUrl;
@Value("${content.client-id}")
private String clientId;
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.refreshToken()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId(clientId);
return WebClient.builder()
.baseUrl(contentUrl)
.filter(oauth2Client)
.build();
}
}
This can be configured using the following application.yml
Spring configuration file:
spring:
security:
oauth2:
client:
registration:
dxd-client:
client-id: ${DXD_CLIENT_ID:cduser}
client-secret: ${DXD_CLIENT_SECRET}
authorization-grant-type: client_credentials
client-authentication-method: client_secret_post
provider:
dxd-client:
token-uri: ${DXD_TOKEN_URL:http://localhost:8082/token.svc}
content:
client-id: dxd-client
url: ${DXD_CONTENT_URL:http://localhost:8081/cd/api}
debug: false
logging:
level:
root: INFO
com.sdl.delivery.content.example.mcp.server: INFO
org.springframework.ai: INFO
org.springframework.security.oauth2.client: ERROR
org.springframework.security.oauth2: ERROR
org.springframework.security.web: INFO
org.springframework.web.reactive.function.client: ERROR
org.springframework: ERROR
reactor.netty.http.client: ERROR
This configuration will ensure that all requests to the Content Service using the Spring GraphQL WebClient shall have the relevant OAuth2 client-credentials added to their request headers. You’ll need to provide relevant credentials for the cduser.
MCP Server
Spring AI makes creating an MCP Server a breeze. First, add the following lines to the application.yml configuration:
spring: main: banner-mode: off ai: mcp: server: name: @project.artifactId@ version: @project.version@ type: ASYNC instructions: "This server provides tools to interact with a Tridion Docs DXD Content Service"
The instructions included here are included as part of the context passed to the LLM, so can use it to tailor the responses.
Next, create a service class that contains your annotated tools, such as:
@Service public class McpServer { private static final Logger LOG = LoggerFactory.getLogger(McpServer.class); private final HttpGraphQlClient graphQLClient; private final ObjectMapper objectMapper = new ObjectMapper(); @Autowired public McpServer(WebClient webClient) { this.graphQLClient = HttpGraphQlClient.create(webClient); } /** * Get topic content by publication ID and URL. * * @param publicationId The publication ID * @param url The topic URL * @return The topic content (XHTML) */ @Tool(description = "Get the content for a specific topic given its publication ID and URL") public String getTopicContentByUrl(@ToolParam(description = "The publication ID") final Integer publicationId, @ToolParam(description = "The topic URL") final String url) { try { String query = """ query ishTopicByURL($publicationId: Int!, $url: String!) { ishTopic(publicationId: $publicationId, url: $url) { __typename publicationId itemId title shortDescription url xhtml ... on IshTaskTopic { body { steps { __typename title xhtml } } } links { item { __typename publicationId itemId title ... on BinaryComponent { __typename publicationId itemId variants { edges { node { binaryId downloadUrl } } } } } } relatedLinks { links { item { __typename publicationId itemId title ... on IshGenericTopic { shortDescription } } } } } } """; Map<String, Object> variables = Map.of("publicationId", publicationId, "url", url); return graphQLClient.document(query) .variables(variables) .retrieve("ishTopic") .toEntity(IshTopic.class) .map(this::processTopicContent) .onErrorReturn("Error: [Request failed]") .doOnError(throwable -> LOG.error("Error fetching topic content", throwable)) .block(); } catch (Exception e) { LOG.error("Error creating GraphQL request", e); return "Error: [" + e.getMessage() + "]"; } } private String processTopicContent(IshTopic ishTopic) { if (ishTopic == null) { LOG.warn("No topic content found"); return "{}"; } LOG.info("Processing topic content for item ID: {}", ishTopic.getItemId()); try { return objectMapper.writeValueAsString(ishTopic); } catch (Exception e) { LOG.error("Error serializing topic content", e); return "{}"; } }
The descriptions on the various annotations are important. This is how the tool advertises itself to the Agent, and in turn how the Large Language Model (LLM) decides when appropriate to use the tool. You’ll see that each tool can include any number of annotated parameters too, all of which are made visible to the Agent.
We map the resultant response from the GraphQL query to the datatypes that we previous generated. This is optional but allows us to more easily parse the responses. You can also use GraphQL Query Builders (such as the DGS Codegen library) if you’d prefer not to craft GraphQL queries by hand. Ultimately, we want to return some structured text that the LLM can understand. In our case, we are relying on the capabilities of the GraphQL ishTopic
query to expose the underlying DITA structure. The LLM uses this structured output to better achieve its goals.
We’re using the Spring WebFlux reactive streaming version of the Spring MCP server for the purposes of this demo.
Finally, update the Security Configuration with this Bean:
@Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { return http.authorizeExchange(exchanges -> exchanges .pathMatchers("/sse/**", "/mcp/**").permitAll() .anyExchange().denyAll()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); }
This configuration is essential if auto using Spring Security OAuth2. Without this, then all incoming requests to the MCP Server shall be authenticated using the OAuth2 configuration. This code tells the service to ignore authentication on the /sse
and /mcp
endpoints. Note that we disable CSRF, as there’s no browser involved in this authentication flow.
Spring Security also allows you to configure different security on incoming MCP requests. In a production system, you would typically secure the /sse
and /mcp
endpoints too. You could use the Spring Security Authorization Server for this.
Having completed these steps, create a Spring Application to run the server:
@SpringBootApplication public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } @Bean public ToolCallbackProvider contentTools(McpServer mcpServer) { return MethodToolCallbackProvider.builder().toolObjects(mcpServer).build(); } }
The ToolCallbackProvider
is responsible for loading all the annotated Beans from the previous steps.
This is broadly all that is needed for a simple MCP Server.
When this Spring Boot application is run, it’ll listen to requests on the given port:
2025-05-29T11:09:06.846+01:00 INFO 78956 --- [ main] c.s.d.e.mcp.server.McpServerApplication : Starting McpServerApplication using Java 21.0.7 with PID 78956 (/Users/bmiddleton/code/dxd/tridion-dxd-mcp-server/target/classes started by bmiddleton in /Users/bmiddleton/code/dxd/tridion-dxd-mcp-server) 2025-05-29T11:09:06.847+01:00 INFO 78956 --- [ main] c.s.d.e.mcp.server.McpServerApplication : No active profile set, falling back to 1 default profile: "default" 2025-05-29T11:09:07.341+01:00 INFO 78956 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration : Enable tools capabilities, notification: true 2025-05-29T11:09:07.341+01:00 INFO 78956 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration : Registered tools: 5 2025-05-29T11:09:07.341+01:00 INFO 78956 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration : Enable resources capabilities, notification: true 2025-05-29T11:09:07.341+01:00 INFO 78956 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration : Enable prompts capabilities, notification: true 2025-05-29T11:09:07.341+01:00 INFO 78956 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration : Enable completions capabilities 2025-05-29T11:09:07.398+01:00 INFO 78956 --- [ main] c.s.d.e.mcp.server.McpServerApplication : Started McpServerApplication in 0.686 seconds (process running for 0.915)
For the full source code to this POC, please see:
https://github.com/RWS/tridion-dxd-mcp-server
Testing the server
In order to test the capabilities of the MCP Server, you can use the MCP Inspector tool, which can be run as follows:
npx @modelcontextprotocol/inspector
You’ll need to provide details of the running service in a format such as:
"dxd": { "url": "http://localhost:8085/sse", "transport": "sse" }
The inspector allows us to query the configured MCP Server for exposed Tools, Prompts and Resources.
Using the server
You can connect any MCP compatible client to this MCP Server and begin using it alongside other MCP Servers. Make sure you secure the service if it is public facing.
You can see an example of it in action here:
https://www.youtube.com/watch?v=HpGla1LZfio
Improvements
To further refine this service, you could make several improvements:
- Secure the service
It is essential to secure the service for public consumption. - Add logging and metrics
Being able to record the number of agentic sessions helps track usage patterns over time. - Use a fluent GraphQL query builder
Tools such as the DGS Codegen can generate client code that can create fluent GraphQL query builders, rather than the text-based queries shown here. - Add support for MCP Prompts
Prompts allow the server to advertise prepared prompts that the client can use to further refine the tool usage. - Add support for MCP Resources
Resources allow data to be exposed in a familiar manner (such as Topics or Binary content) for use as context. MCP Clients handle resources in their own way. - Follow progress on Spring AI
Spring AI provides a complete framework for interacting with AI technologies in a implementation-agnostic manner. It’s a project that keeps track of the fast-moving world of AI developments.
Conclusion
The integration of agentic AI with Tridion Docs DXD represents a decisive moment in the evolution of technical documentation. What we've explored is a practical way to connect Tridion Docs DXD to agents, transforming how organizations create, manage, and deliver technical information.
The journey from traditional documentation to intelligent, interactive assistance isn't as daunting as it might initially appear. By using Anthropic's Model Context Protocol as the bridge between the Tridion Docs infrastructure and modern AI capabilities, customers can begin realizing benefits quickly while building toward a more comprehensive vision.