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 in h1McpPublicTools/ (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-md3 page 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

  1. Add the dependency to your webapp POM.
  2. Set the LLM environment variables (next section). Without them the MCP server works but the chat answers 503.
  3. Build and deploy the WAR as usual.
  4. Start the instance. On first start the module creates no content and bootstraps no configuration. It logs H1 MCP Server starting and, if it finds a leftover /server/filters/aiChatInject node from an older version, removes it — the filter is registered from code now.
  5. Check Tools › Modules for h1-mcp and confirm the version.
  6. 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_KEY while HABITO_AI_PROVIDER is unset still tries OpenAI and fails asking for OPENAI_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
  1. The outer composite must be named h1-mcp: the tab include points at that name.
  2. Mandatory at every level. currentItemProvider keeps the fields as flat properties of the node, which is how the h1mcp_keywords tool 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 name of 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.yaml is named after name: h1-case-studies-app, not after the h1-case-studies directory.

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:

  • initialize returns serverInfo.version as the module version plus the Maven build timestamp. If it says just 1.0.0, the JAR was built without resource filtering — typically a run straight from an IDE.
  • An empty tools array 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 that h1-mcp-api is installed.
  • 503 with "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 200 with isError: true and the message as text, not an HTTP error. That is deliberate: the model has to read the failure and correct itself.