MCP
The Brightspot AI plugin can expose CMS content over the Model Context Protocol (MCP), letting external AI clients—IDE assistants, chat apps, custom agents—search, read, create, update, and delete content through a tool-calling interface. It can also do the reverse: connect to an external MCP server and use its tools from a Brightspot AI chat request (see Consuming external MCP servers).
The MCP server is a separate module that runs inside the Brightspot web application. It is implemented as an ApiEndpoint, so it inherits Brightspot's existing API client, credential, and permission model. The same @ToolMethod-annotated tools used by the Esca Agent (Ask Esca, Create with Esca) are reused as MCP tools.
How it works
- The
mcpmodule exposes MCP throughCustomApiEndpoint(display name Custom API) paired withMcpApiHandler(display name MCP). The endpoint owns paths and the authenticator chain; the handler owns the protocol. - A servlet filter (
McpApiEndpointFilter) starts a stateless MCP server on application init. It discovers every concreteToolGroupon the classpath, resolves each@ToolMethod-annotated method into a tool, and registers it with the server. - The server advertises tools only—prompts and resources are disabled. Each tool name is namespaced as
<GroupName>_<methodName>(for example,Content_search). - The endpoint authenticates the request and then delegates to the MCP handler, which dispatches the tool call. The first authenticator that resolves a user sets it as the current
ToolUser. - Tool return values become the MCP
structuredContentpayload. Scalars and collections are wrapped as{"result": ...}. Exceptions are returned as an error result with the stack trace as text content.
Installation
Add the mcp artifact alongside the core AI plugin:
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.brightspot.ai</groupId>
<artifactId>mcp</artifactId>
<version>3.2.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.ai:mcp:3.2.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.ai:mcp:3.2.0")
This pulls in the upstream MCP SDK (io.modelcontextprotocol.sdk:mcp-core plus the Jackson JSON transport at runtime) and registers the MCP endpoint and built-in Content tool group.
Setting up the endpoint
The MCP server is exposed through a Brightspot API endpoint, configured in the CMS like any other endpoint.
To create an MCP endpoint:
- From the left navigation, under Admin, click APIs.
- Create a new API endpoint and select Custom API as the type.
- In the Paths field, add one or more URL paths to serve MCP traffic at (for example,
/mcp). - Add one or more Authenticators (typically OAuth and/or API Key). Endpoints with no authenticators reject every request with
401 Unauthorized. - From the Handler list, select MCP.
- Save.
The endpoint is now reachable at the configured paths on your application host. Point MCP clients at the full URL, including scheme and host.
Authentication
CustomApiEndpoint runs the configurable ApiAuthenticator chain. Two built-in authenticators ship with the plugin:
- OAuth (
OAuthApiAuthenticator) —Authorization: Bearer <JWT>. The JWT is issued by Brightspot's OAuth 2.1 authorization server (see OAuth). Itssubclaim names the end user; tool calls run with that user's CMS permissions. - API Key (
ApiKeyApiAuthenticator) —Authorization: Bearer <opaque-token>orX-API-Key: <token>. The client is resolved as a BrightspotApiClientand tool calls run as the client's configured service user (see Access control).
When more than one is configured the chain runs in order; the first authenticator that matches the request wins.
When the Authorization header carries a perimeter HTTP Basic credential (for example, a lower-environment access gate that occupies the standard Authorization slot), both authenticators read the bearer token from an X-Auth-Token request header instead. The fallback only applies when Authorization uses the Basic scheme; clients in environments without the perimeter gate should continue to send Authorization: Bearer <token>.
The standard OAuth WWW-Authenticate: Bearer resource_metadata=... challenge is not extended to advertise this fallback, so OAuth autodiscovery does not work behind a Basic gate. Clients in these environments must be configured out-of-band to send their token via X-Auth-Token (alongside the Basic credential they already have for the gate).
Invalid credentials produce 401 Unauthorized with each configured authenticator's challenge as a WWW-Authenticate header — for OAuth that's the RFC 9728 Bearer resource_metadata=... pointer to the protected-resource metadata document. Valid credentials that lack endpoint or permission access produce 403 Forbidden.
For external MCP clients (IDE assistants, hosted chat apps), configure the OAuth authenticator — clients discover the authorization server from the challenge, register dynamically, and issue per-user tokens with no manual credential sharing. The API-key path is useful for in-house service-to-service calls where a shared credential is acceptable.
OAuth
OAuth support is provided by the cms-oauth-server module that ships with Brightspot CMS. Add it to your application alongside the mcp dependency to enable the OAuth authenticator and discovery endpoints:
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.psddev</groupId>
<artifactId>cms-oauth-server</artifactId>
<version>3.2.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.psddev:cms-oauth-server:3.2.0'
// Requires Brightspot 4.8 or later.
implementation("com.psddev:cms-oauth-server:3.2.0")
It is an OAuth 2.1 authorization server supporting the authorization-code flow with PKCE (S256), the client-credentials flow, refresh tokens with rotation and replay detection, dynamic client registration (RFC 7591), token revocation (RFC 7009), resource indicators (RFC 8707), and automatic signing-key rotation.
Discovery endpoints (RFC 8414, RFC 9728):
| Path | Purpose |
|---|---|
/.well-known/oauth-authorization-server | Authorization server metadata. |
/.well-known/oauth-protected-resource/<path> | Protected resource metadata for the MCP endpoint at <path>. |
/.well-known/jwks.json | JSON Web Key Set for verifying issued JWTs. |
OAuth endpoints:
| Path | Purpose |
|---|---|
/_oauth/authorize | Authorization endpoint; renders the consent screen. |
/_oauth/token | Token endpoint; handles all supported grant types. |
/_oauth/register | Dynamic client registration. |
/_oauth/revoke | Refresh-token revocation. |
A typical MCP client flow:
- The client makes an unauthenticated request to the MCP endpoint and receives
401with theresource_metadatachallenge. - The client fetches the metadata documents to discover the authorization server.
- The client registers itself at
/_oauth/register(or uses a pre-provisionedApiClient). - The client starts the authorization-code flow at
/_oauth/authorizewithcode_challenge(S256) andresource=<MCP endpoint URL>. - The CMS-authenticated user approves the consent screen (or reuses a remembered consent).
- The client exchanges the code at
/_oauth/tokenand calls the MCP endpoint with the resulting JWT.
The OAuthPlugin record configures the issuer URL, code/token/key TTLs, and the key rotation interval. When the issuer is unset, the subsystem derives it from the current request's scheme and host — fine for development, but set it explicitly behind reverse proxies or for tokens minted outside a web request.
Toggle on the Disable DCR setting on the same record to restrict access to operator-provisioned clients only. When disabled, /_oauth/register returns 404 and the registration_endpoint field is omitted from the authorization-server metadata, signaling to clients that dynamic registration is unavailable.
To pre-provision an MCP client without OAuth:
- From the left navigation, under Admin, click APIs.
- Create or open an API Client.
- Generate a token for the client and grant it access to the MCP endpoint.
- Set the Service User field on the client (see Access control) — this is the tool user the client acts as.
- Share the token with the MCP client out of band.
Access control
Every ApiClient carries a Service User field (via the ApiClientExtra modification). It's the tool user the client acts as for non-OAuth authentication — API key, opaque bearer, or the OAuth client_credentials grant. Tool calls run with that user's site and content type permissions, the same model used by the in-CMS tool UI.
Configure access by creating a dedicated service ToolUser (or reusing an existing one) with exactly the role, sites, and content types the MCP client should see, then point the API client at it. Built-in and custom ToolGroup implementations that go through ToolRequest automatically pick up these restrictions—no extra wiring is required.
Each API client carries its own service user, so different manually-provisioned clients can run with different CMS permissions and audit trails. OAuth-issued tokens resolve their user from the JWT's sub claim instead and don't consult the service user.
Treat MCP credentials like any other API token. A client inherits everything its service user can do; create a narrowly scoped user rather than reusing a privileged administrator.
Every built-in content tool group additionally enforces the same area permissions (area/admin/adminUsers, etc.) the CMS UI gates Managed types behind—for example ToolUser. A caller with broad type access still cannot read or write a Managed record over MCP unless it also holds the area/... permission governing that type's edit page.
Built-in tools
The mcp module ships the Content tool group. Three more first-party groups—Translation, Experimentation, and Theme—ship as separate optional artifacts and register automatically once added to the classpath, the same way a custom tool group would.
Content
ContentToolGroup provides CRUD and search over Brightspot content. All tools are namespaced Content_*:
| Tool | Hints | Purpose |
|---|---|---|
Content_searchType | read-only, idempotent | Find names of CMS content types the user can read, by partial match. |
Content_getType | read-only, idempotent | Get the JSON Schema for a content type. |
Content_getFieldValues | read-only, idempotent | List allowed values for a field with a value generator. |
Content_searchTextually | read-only, idempotent | Keyword search across content. |
Content_searchSemantically | read-only, idempotent | Embedding-based search across content. |
Content_get | read-only, idempotent | Fetch a single content item by ID. |
Content_generateIds | read-only | Generate fresh UUIDs for new content. |
Content_create | write | Create new content of a given type through the publish pipeline, optionally as a draft. |
Content_update | write | Update existing content by ID; republish, save changes as a draft, or publish a draft. |
Content_delete | write, destructive | Delete content (or a single draft) by ID. |
Content_searchType returns only CMS content types the caller's user has permission to read, so an agent never discovers a type it cannot query—a missing type then reads as "no access" rather than "no data." Embedded and other non-content Record types—the $ref targets in a Content_getType schema—are not listed here; resolve them by exact name through Content_getType, which is not permission-filtered.
The group also publishes a usage guide to the MCP server's instructions payload so that clients can route tool calls correctly without trial and error. See ContentToolGroup#getInstructions for the current text.
Content_searchSemantically requires a configured text embedding generator (see Configuration). The other tools work without one.
Translation
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.brightspot.ai</groupId>
<artifactId>mcp-translation</artifactId>
<version>3.2.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.ai:mcp-translation:3.2.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.ai:mcp-translation:3.2.0")
TranslationToolGroup translates content through Brightspot's core translation feature, namespaced Translation_*. It supports two paths: an agent path, where the caller produces the translated text itself and writes it back, and a provider path, where a configured external service (for example AWS, DeepL, or Google) performs the translation.
| Tool | Hints | Purpose |
|---|---|---|
Translation_listServices | read-only, idempotent | List configured external translation providers. |
Translation_listLocales | read-only, idempotent | List locales available for translation. |
Translation_inspectContent | read-only, idempotent | Get a content item's source locale, translatable fields, and existing localized variations. |
Translation_getTranslatableText | read-only, idempotent | Get a content item's source text to translate, keyed by field path. |
Translation_status | read-only, idempotent | Get translation logs for a content item. |
Translation_translate | write, destructive | Translate content into one or more locales using a configured provider (the provider path). |
Translation_applyTranslation | write, destructive | Write agent-produced translations back as a new localized variation (the agent path). |
When no provider is configured, Translation_translate refuses to run rather than copying untranslated text—use Translation_getTranslatableText and Translation_applyTranslation instead. Each tool requires the same translation/translate or translation/action/view-log permission as the equivalent CMS translation UI action.
Experimentation
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.brightspot.ai</groupId>
<artifactId>mcp-experimentation</artifactId>
<version>3.2.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.ai:mcp-experimentation:3.2.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.ai:mcp-experimentation:3.2.0")
ExperimentationToolGroup creates and manages Brightspot experiments (A/B tests) on content, namespaced Experimentation_*. It is service-agnostic: it touches only the experimentation core, never a specific provider module.
| Tool | Hints | Purpose |
|---|---|---|
Experimentation_listServices | read-only, idempotent | List configured experiment service providers for a site. |
Experimentation_listExperimentTypes | read-only, idempotent | List creatable experiment types. |
Experimentation_listSupportedContentTypes | read-only, idempotent | List content types that can have experiments run on them. |
Experimentation_list | read-only, idempotent | List experiments, optionally filtered by target content or status. |
Experimentation_get | read-only, idempotent | Get an experiment by ID, including its variations. |
Experimentation_listVariations | read-only, idempotent | List the variation records that exist for a content item. |
Experimentation_createVariation | write | Create a variation arm for a target content item. |
Experimentation_create | write | Create an experiment of a given type running on a target content item. |
Experimentation_update | write | Update an experiment's fields. |
Experimentation_setStatus | write | Set an experiment's status (RUNNING, PAUSED, PLANNED, COMPLETED). |
Experimentation_end | write, destructive | Complete an experiment, optionally promoting a winner and archiving variations. |
Experimentation_promoteVariation | write, destructive | Promote a variation to the content's live primary variation immediately. |
Experimentation_delete | write, destructive | Notify the provider and move an experiment to trash. |
Experiments and variations are content, so type-specific and provider-specific fields are set through a fields map whose shape the caller learns from Content_getType—the group does not duplicate that schema. The group's own getInstructions text walks through the typical create flow in detail.
Theme
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.brightspot.ai</groupId>
<artifactId>mcp-theme</artifactId>
<version>3.2.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.ai:mcp-theme:3.2.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.ai:mcp-theme:3.2.0")
ThemeToolGroup inspects the active CMS theme and the style-scoped theme fields it defines per content type, namespaced Theme_*. It is read-only—all writes go through Content_update in the Content tool group, using the field names these tools resolve.
| Tool | Hints | Purpose |
|---|---|---|
Theme_getCurrent | read-only, idempotent | Get the active theme's summary and global (theme-wide) fields. |
Theme_describeType | read-only, idempotent | Get the styles and fields the active theme defines for a content type. |
For an MCP client, the active theme is either set explicitly on the client (via the MCP cluster's Theme field on the API client) or, when the client has exactly one allowed site, that site's configured theme.
Publishing, drafts, and workflow
Content_create and Content_update persist content through Brightspot's publish pipeline (Content.Static#publish), so AI-driven writes carry the same publish metadata, revision history, and publish-time triggers as writes made in the CMS UI. Writes are attributed to the acting user — in the MCP server, the API client's configured service user — so publish metadata and Version History record who made the change; the tools refuse to write when no user resolves. Both tools accept an optional action parameter:
PUBLISH— the content goes live immediately. This is the effective default forContent_create; forContent_updatethe default keeps the content's current stage (published content is republished, a draft stays a draft).DRAFT— the content is saved as an unpublished draft. For currently published content,Content_updatecreates a separateDraftrecord holding only the changes and leaves the live content untouched.
Results include a _status field (PUBLISHED or DRAFT). Draft results add a _message explaining what happened and, when the changes live in a separate Draft record, a _draftId — the ID to pass to subsequent Content_update calls to keep editing (or publish) that draft.
When the content's type is governed by a CMS workflow for the site, content is saved as a draft in the workflow's initial state, and the result's _workflow field reports the workflow name, current state, and available transitions. A CMS user then advances the content through the workflow. One exception matches the CMS's publish override: an explicit PUBLISH request from a caller whose service user holds the type's Publish permission bypasses the workflow and publishes directly. A PUBLISH request without that permission is redirected to a draft and the result's _message explains why.
Drafts and workflow-governed content are excluded from search results, but remain reachable by ID through Content_get, Content_update, and Content_delete. Deleting a draft's _draftId removes only the draft; the published content is untouched.
Tracking AI-generated content
Content and fields written through Content_create, Content_update, or a writing tool from Translation or Experimentation—whether the caller is an external MCP client or the Esca Agent (Ask Esca, Create with Esca)—are flagged as AI-generated. The flag is recorded per field, so an editor can tell which fields on an asset came from a tool call and which were written by hand.
This is the same tracking Create with Esca uses for its own writes—see Finding AI-generated content for the Contains AI? search filter and the AI Fields search result column.
Adding custom tools
Any concrete subclass of ToolGroup on the classpath is picked up automatically. Tools defined this way are available both to the Esca Agent and to MCP clients.
1@NullMarked2public class WeatherToolGroup extends ToolGroup {34@Override5public String getName() {6return "Weather";7}89@Override10public @Nullable String getInstructions() {11return "Use Weather_* tools for current conditions and forecasts.";12}1314@ToolMethod(15title = "Get the current temperature for a city",16readOnly = true,17destructive = false,18idempotent = false,19description = "Returns the current temperature in Celsius for the given city.")20public double currentTemperature(21@ToolParam(description = "City name, e.g. `San Francisco`.") String city) {2223return WeatherApi.lookup(city).temperatureCelsius();24}25}
Notes:
- The group's
getName()becomes the tool name prefix (Weather_currentTemperaturehere). - Per-method behavioral hints (
readOnly,destructive,idempotent) on@ToolMethodare forwarded to MCP clients, which may use them to auto-approve calls or prompt for confirmation. - Parameter schemas are derived from
@ToolParamannotations and the method signature. For non-standard schemas, supply aSchemaSuppliervia@ToolParam. - Return values are serialized as structured content. Scalars and collections are auto-wrapped as
{"result": ...}; return aMap<String, Object>for richer payloads. - Throwing from a tool method produces an MCP error result; the stack trace is included as text content.
Consuming external MCP servers
The mcp-client module is the reverse direction of everything above: instead of Brightspot hosting an MCP server, application code connects to an external MCP server and exposes its tools to an AI chat request.
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.brightspot.ai</groupId>
<artifactId>mcp-client</artifactId>
<version>3.2.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.ai:mcp-client:3.2.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.ai:mcp-client:3.2.0")
Open a connection with McpServerConnection, then add its tools to a ChatRequest:
1try (McpServerConnection github = McpServerConnection.streamableHttp("https://example.com/mcp")2.name("github")3.header("Authorization", "Bearer " + token)4.connect()) {56ChatRequest request = ChatRequest.builder()7.addTools(github.getTools())8.build();9}
A connection wraps a single MCP session: open it in a try-with-resources block around one run, and do not share it across concurrent runs. The name(...) passed to the builder namespaces the remote tool names as mcp__<name>__<tool> so several servers can be attached to one request without collisions.
Three transports are available: streamableHttp and sse connect to a remote server over HTTP and are permitted by default; stdio launches a local child process and is disabled by default because it is arbitrary code execution on the host. Enable it (or restrict the HTTP transports) with the brightspot/ai/mcp/allowedTransports setting, a comma-separated list of STREAMABLE_HTTP, SSE, STDIO, or HTTP as shorthand for both HTTP transports:
1brightspot/ai/mcp/allowedTransports=HTTP,STDIO
Calling a stdio() factory method when STDIO is not enabled throws McpTransportNotAllowedException.
Enabling the STDIO transport lets application code launch arbitrary local processes. Only enable it on deployments that need to connect to local MCP servers, and only when the code constructing the connection is trusted.
For deeper internals—Tool, RunnableTool, ToolGroup discovery, and the agentic chat loop that consumes the same tools—see Architecture overview.