Developers · Installation¶
What you get¶
One JAR — com.habito1:h1-mcp — with 29 classes, about 3 600 lines of Java and one light
module, h1-mcp-light-module:
- 3 REST endpoints exposing 8 routes: the admin MCP surface, the public MCP surface and the AI Chat.
- 3 singleton components:
McpSessionManager,LlmClientFactory,AiConversationManager. - 2 LLM clients behind a one-method interface: Claude and OpenAI, over the JDK HTTP client — no vendor SDK on the classpath.
- 5 MCP tools registered in
h1McpTools/:website,asset,content_type,marketing_tag,h1mcp_keywords. Plus 2 registrations inh1McpPublicTools/(website,asset) restricted to read-only actions. - 1 content app (
ai-chat-app, an embedded page) and an AdminCentral launcher group, [H1] AI. - 2 injection mechanisms for the chat panel, both registered through
META-INF/services: a servlet filter and a Vaadin service listener. - 4 web resources (2 JS, 2 CSS) served from the light module.
- 5 decorations: the AdminCentral launcher, two
frontend-md3page dialogs, and the products and case studies apps. - 1 i18n bundle (default +
_en+_es) and 2 reusable includes. - No JCR workspace of its own, no virtual URI, and a version handler with no deltas.
Maven coordinates¶
<!-- your-webapp/pom.xml -->
<dependency>
<groupId>com.habito1</groupId>
<artifactId>h1-mcp</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
Deploy¶
- Add the dependency to your webapp POM.
- Set the LLM environment variables (next section). Without them the MCP server works but the
chat answers
503. - Build and deploy the WAR as usual.
- Start the instance. On first start the module creates no content and bootstraps no configuration.
It logs
H1 MCP Server startingand, if it finds a leftover/server/filters/aiChatInjectnode from an older version, removes it — the filter is registered from code now. - Check Tools › Modules for
h1-mcpand confirm the version. - Confirm the surface: AdminCentral shows an [H1] AI group with the AI Chat app, and a 🤖 button appears in the top bar.
Wiring you have to do¶
1. The API key and the provider¶
This is the step that decides whether the chat works at all. Configuration is by environment variable, read once when the first chat request builds the client:
| Variable | Values | Default |
|---|---|---|
HABITO_AI_PROVIDER |
openai · claude |
openai (also readable as the habito.ai.provider system property) |
OPENAI_API_KEY |
your key | — required when the provider is openai |
ANTHROPIC_API_KEY |
your key | — required when the provider is claude |
HABITO_AI_MODEL |
any model id of that provider | gpt-4o · claude-sonnet-4-6 |
# docker-compose / systemd / catalina env
HABITO_AI_PROVIDER=claude
ANTHROPIC_API_KEY=sk-ant-…
HABITO_AI_MODEL=claude-sonnet-4-6
Warning — the client is built once and cached for the life of the JVM. Changing any of these variables needs a restart; editing them in a running container has no effect and no warning. The provider is also chosen before the key is read, so setting only
ANTHROPIC_API_KEYwhileHABITO_AI_PROVIDERis unset still tries OpenAI and fails asking forOPENAI_API_KEY.Warning — never commit these keys. They are read from the environment precisely so they never land in a definition, a bootstrap file or a repository.
2. Nothing to wire for the tools — but check the transport pairing¶
The tools this module exposes are whatever h1-mcp-api's registries hold, which is every module's
h1McpTools/ folder. You do not register anything here. What you do need to get right is which
surface a client talks to:
| Client | Use | Reads |
|---|---|---|
| Claude Desktop, MCP Inspector, your own agent (authenticated) | POST /.rest/v1/mcp/admin |
the full admin tool set |
| A visitor-facing agent | POST /.rest/v1/mcp |
only h1McpPublicTools/, and only their allowedActions |
3. The chat panel is injected, not configured¶
Two independent mechanisms put the panel into AdminCentral, both discovered through
META-INF/services with no configuration node:
| Mechanism | What it does |
|---|---|
AiChatServletContainerInitializer |
Registers AiChatInjectFilter for /.magnolia/admincentral and /.magnolia/admincentral/*, before the filters declared in web.xml. The filter buffers the HTML response and inserts the CSS and JS tags before </body>. |
AiChatSidebarListener |
A VaadinServiceInitListener that, on the first request of each Vaadin session, adds the stylesheet, sets window._h1AiChatUrl to the chat endpoint and appends the script tag. |
Both are best-effort by design. The filter gives up and serves the original bytes when the response
carries a Content-Encoding (a gzipping proxy in front of Tomcat), when the body has no </body>
or </html>, or when the response is already committed — and it logs nothing in the first two
cases. The JS then waits for Vaadin to render the top bar with a MutationObserver and hooks onto
.findbarAndApplauncher; if that container is not found — a different AdminCentral theme, a future
Magnolia UI — no button appears and there is no error.
Nothing to do here on a stock instance. On a customised AdminCentral, that is the place to look.
4. The [H1] AI tab, where you want it¶
The keyword fields reach a form through a decoration. Out of the box the module decorates the H1
platform's frontend-md3 page dialogs (Base, FrontPage) and two apps
(h1-ecommerce-products-app, h1-case-studies-app). A decoration names its target module in its
own path, so on an instance without frontend-md3 those page-dialog decorations simply never apply —
no error, no log line. Treat them as the reference implementation:
# <your-light-module>/decorations/<your-light-module>/dialogs/pages/base.yaml
form:
properties:
h1-mcp: # (1)
label: h1-mcp.tab.h1-mcp.label
$type: compositeField
layout:
$type: stacked
itemProvider:
$type: currentItemProvider # (2)
properties:
h1-mcp-keywords:
label: h1-mcp.tab.h1-mcp-keywords.label
$type: compositeField
layout:
$type: stacked
itemProvider:
$type: currentItemProvider
properties: !include:/h1-mcp-light-module/includes/keywords-fields.yaml
layout:
tabs: !include:/h1-mcp-light-module/includes/tabs.yaml
- The outer composite must be named
h1-mcp: the tab include points at that name. - Mandatory at every level.
currentItemProviderkeeps the fields as flat properties of the node, which is how theh1mcp_keywordstool reads them. Without it the values land in a child node and the tool refuses the content type.
For a content app the shape is the same under subApps.detail.form. The include set is deliberately
two files — keywords-fields.yaml for the fields and tabs.yaml for the tab — so adding a second AI
feature to the tab later is one more composite inside, without touching any decoration.
Warning — the decoration file name must match the
nameof the target app, not its folder and not its label. If it does not match, the decoration is silently ignored: no tab, no error, no log line.h1-case-studies-app.yamlis named aftername: h1-case-studies-app, not after theh1-case-studiesdirectory.
5. Deciding what a visitor may reach¶
The public surface exists only if you put something in it. One file per tool, and an action whitelist:
# <your-light-module>/h1McpPublicTools/website.yaml
class: com.habito1.mcp.tools.website.WebsiteTool
allowedActions:
- list_pages
- get_page
- list_templates
That is exactly what this module ships for website and, for asset, list + get. The whitelist
stops the attempt; what actually protects the content is the ACL of the user the public endpoint
runs as. Configure both.
Permissions¶
This module owns no workspace. Every tool runs in the calling session, so the reachable content is whatever that user can read and write — there is no elevation anywhere:
| Surface | Runs as | What it needs |
|---|---|---|
AI Chat (/v1/ai/chat) |
The logged-in AdminCentral user | Their normal permissions. The chat is as powerful as the person using it, no more |
Admin MCP (/v1/mcp/admin) |
The authenticated REST user | Read/write on the workspaces its tools touch, plus activation rights for publish/unpublish |
Public MCP (/v1/mcp) |
Whatever user the public instance runs REST as — typically anonymous | Anonymous read on the workspaces you expose, and nothing more. Pair it with allowedActions |
asset uploads |
The calling session | Write access on dam. The upload writes the binary into the asset's resource node |
publish / unpublish / delete |
The calling session | Magnolia's default command catalog (activate / deactivate) |
Warning — a missing ACL does not look like an error. A listing returns what the session can read, so an unreadable subtree is indistinguishable from an empty folder, and a page the user cannot see comes back as "not found". If the assistant claims something does not exist while you can see it in AdminCentral, compare the users before suspecting the code.
Verify the installation¶
# 1. The admin tool list — every h1McpTools/ registration in the instance.
curl -s -u "$MGNL_USER:$MGNL_PASS" -X POST \
http://localhost:8080/magnoliaAuthor/.rest/v1/mcp/admin \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 2. The handshake, which also reports the deployed version and build timestamp.
curl -s -u "$MGNL_USER:$MGNL_PASS" -X POST \
http://localhost:8080/magnoliaAuthor/.rest/v1/mcp/admin \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
# 3. The public surface (no credentials): only what h1McpPublicTools/ declares.
curl -s -X POST http://localhost:8080/magnoliaPublic/.rest/v1/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 4. The chat itself — this one costs an LLM call.
curl -s -u "$MGNL_USER:$MGNL_PASS" -X POST \
http://localhost:8080/magnoliaAuthor/.rest/v1/ai/chat \
-H 'Content-Type: application/json' \
-d '{"message":"listá las páginas de la raíz"}'
What the answers mean:
initializereturnsserverInfo.versionas the module version plus the Maven build timestamp. If it says just1.0.0, the JAR was built without resource filtering — typically a run straight from an IDE.- An empty
toolsarray on the public surface is correct on an instance where no module registers a public tool. On the admin surface it means nothing was discovered at all: check thath1-mcp-apiis installed. 503with "Proveedor de IA no configurado" from the chat means the API key variable is missing. The MCP surfaces are unaffected — they never call an LLM.- A tool that fails answers
200withisError: trueand the message as text, not an HTTP error. That is deliberate: the model has to read the failure and correct itself.