MCP
We are actively working to improve this documentation. The content you see here may be incomplete, subject to change, or may not fully reflect the current state of the feature. We appreciate your understanding as we continue to enhance our docs.
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.
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 in-process agents (Ask AI, Create with AI) 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.1.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.ai:mcp:3.1.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.ai:mcp:3.1.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:
- Click > Admin > 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.1.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.psddev:cms-oauth-server:3.1.0'
// Requires Brightspot 4.8 or later.
implementation("com.psddev:cms-oauth-server:3.1.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:
- Click > Admin > 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.
Built-in tools
The mcp module ships one tool group, Content (ContentToolGroup), providing 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. |
Content_update | write | Update existing content by ID. |
Content_delete | write, destructive | Delete content 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.
Adding custom tools
Any concrete subclass of ToolGroup on the classpath is picked up automatically. Tools defined this way are available both to the in-process agents 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.
For deeper internals—Tool, RunnableTool, ToolGroup discovery, and the agentic chat loop that consumes the same tools—see Architecture overview.