Guide exposes resources relating to the Embabel Agent Framework, such as documentation, relevant blogs and other content, and up-to-the-minute API information.
Use Guide in one of the following ways:
- Via a chat server (WebSocket/STOMP) for custom front-ends
- Via an MCP server for integration with Claude Desktop, Claude Code and other MCP clients
- Via spring shell
Note: The chat server and Spring Shell conflict with each other. By default, the chat server is enabled. To use Spring Shell instead, uncomment the relevant lines in
pom.xml.
This repository is the backend for Embabel Hub — the Guide powers the natural-language "Talk to the Docs" experience there.
- Embabel Hub — talk to the docs, powered by this Guide
- Embabel Agent Framework
Blog: The Voice, The Word, and The Wheel — adding voice interaction (TTS/STT) to the Guide with Deepgram, a narrator agent, and natural-language commands.
curl -X POST http://localhost:1337/api/v1/data/load-referencesTo see stats on data, make a GET request or browse to http://localhost:1337/api/v1/data/stats
RAG content storage uses the ChunkingContentElementRepository interface from the embabel-agent-rag-core library. The backend is GraphObjectManagerStore from embabel-agent-rag-graph, on whichever graph database the active profile points at. You can plug in other backends by providing a different ChunkingContentElementRepository bean.
Guide answers from one version of the Embabel docs at a time. The version is set in application.yml:
guide:
content:
versioned:
base-url: https://docs.embabel.com/embabel-agent/guide/
versions:
- 1.5.3 # the first entry is the active version
supplementary:
- https://medium.com/... # blog posts and other material that belongs to no versionThe setting does two things:
- At ingestion, each entry of
versionsis fetched frombase-url+ version +/, and every chunk is given aversionproperty: the version in its document's URL for the versioned docs, andsupplementaryfor everything else (VersionChunkTransformer). - At search, chat and the MCP tools only see chunks whose
versionis the active version orsupplementary(ChatActions.versionFilterandMcpToolExportConfiguration). The active version is the first entry ofversions. With no entries there is no filter.
So changing the version takes two steps: change versions, then ingest (load-references). Until the new version is ingested, the store holds only chunks of the old one, the filter hides them all, and Guide answers from the supplementary material alone. The same holds for a seeded database, such as the one in the embabel/hub image: its chunks have to carry the version Guide is configured with.
More than one version can be listed, and each is ingested, but only the first is searched. Choosing the version per user or per request is not built: the filter is fixed when the application starts.
The stamp is stored on the chunk as metadata.version. To see what a store holds:
MATCH (c:Chunk) RETURN c.`metadata.version` AS version, count(*) AS chunksChunks ingested before Guide moved to GraphObjectManagerStore carry the stamp as a plain version property instead. The filter does not read that, so those chunks match no version and are hidden from search: a store that old has to be ingested again, whatever version it holds.
The seed database of the embabel/hub image is kept in step by the Export Seed Database workflow here, which ingests the configured version into a fresh database each day and sends the export to embabel-hub as a pull request. GitHub disables a scheduled workflow after 60 days without activity in the repository, and the seed then goes stale without anything failing: check that the workflow is still enabled when coming back to this project.
GraphObjectManagerStore (from embabel-agent-rag-graph) supports four backends: Neo4j (default), FalkorDB, Memgraph and Oracle 23ai. The active backend is selected by Spring profile in src/main/resources/application.yml — each profile sets database.dataSources.neo.type, and Drivine speaks to whichever engine that names.
| Backend | Profile | Default port | Compose profile |
|---|---|---|---|
| Neo4j | neo4j |
7687 (bolt) |
neo4j |
| FalkorDB | falkordb |
6379 |
falkordb |
| Memgraph | memgraph |
7688 (bolt) |
memgraph |
| Oracle 23ai |
oracle |
1521 |
oracle |
embabel-chat-store and embabel-agent-rag-graph (see GraphObjectManagerStore) use Drivine's StatelessGraphObjectManager, which generates openCypher in each engine's dialect, or SQL/PGQ for Oracle.
Change spring.profiles.active in application.yml, or override at launch:
# Neo4j (default)
./mvnw spring-boot:run -Dspring-boot.run.profiles=neo4j
# FalkorDB
./mvnw spring-boot:run -Dspring-boot.run.profiles=falkordb
# Memgraph
./mvnw spring-boot:run -Dspring-boot.run.profiles=memgraph
# Oracle
./mvnw spring-boot:run -Dspring-boot.run.profiles=oracledocker compose --profile neo4j up -d # or falkordb / memgraph / oracleMemgraph maps host port 7688 → container 7687 to avoid clashing with Neo4j, so you can run them side-by-side. FalkorDB uses Redis protocol on 6379.
Oracle runs from gvenzl/oracle-free:23-faststart and wants about 2 GB of memory; give Docker enough, or the container is killed while it starts. On first start the image creates the application user, and the oracle Spring profile connects as it. The profile reads all of these; compose reads the port, username and password:
| Variable | Default |
|---|---|
ORACLE_HOST |
localhost |
ORACLE_PORT |
1521 |
ORACLE_USERNAME |
guide |
ORACLE_PASSWORD |
brahmsian |
ORACLE_SERVICE |
FREEPDB1 |
The user is created only when the oracle_data volume is new: after changing the username or password, stop the container and remove its volume (docker rm -f embabel-oracle && docker volume rm guide_oracle_data, which deletes the Oracle data), or create the user yourself. docker compose down -v would remove the Neo4j volume as well. Guide makes its tables on startup, and the RAG store is filled by an ingest as on any other backend.
-
Neo4j: http://localhost:7474/browser/ (user
neo4j, passwordbrahmsian) -
FalkorDB: http://localhost:3001 (FalkorDB Browser, started with the
falkordbcompose profile) -
Memgraph: connect via Memgraph Lab to
bolt://localhost:7688 -
Oracle: SQL*Plus in the container. Nodes are rows of
drivine_node, with their labels and properties as JSON, and relationships are rows ofdrivine_edge:docker exec -it embabel-oracle sqlplus guide/brahmsian@FREEPDB1select count(*) from drivine_node where json_exists(labels, '$[*]?(@ == "Chunk")');
Go to the Neo Browser at http://localhost:7474/browser/
Log in with username neo4j and password brahmsian (or your custom password if you set one).
To delete all data run the following query:
MATCH (n:ContentElement)
DETACH DELETE nStarting the server will expose MCP tools on http://localhost:1337/sse.
An easy way to verify the tools are exposed and experiment with calling them is by running the MCP inspector:
npx @modelcontextprotocol/inspectorWithin the inspector UI, connect to http://localhost:1337/sse.
Before configuring a client, confirm the server is up and returning SSE headers:
curl -i --max-time 3 http://localhost:1337/sseIf you're running the server on a different port (for example 1338), update the URL accordingly.
Add this stanza to claude_desktop_config.json:
{
"mcpServers": {
"embabel-dev": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:1337/sse",
"--transport",
"sse-only"
]
},
...
}See Connect Local Servers for detailed documentation.
You should create a Project to ensure that Claude knows its purpose and how to use tools. See claude_project.md for suggested content.
If you're using Claude Code, adding the Embabel MCP server will powerfully augment its capabilities for working on Embabel applications and helping you learn Embabel.
claude mcp add embabel --transport sse http://localhost:1337/sseWithin the Claude Code shell, type /mcp to test the connection. Choose the number of the embabel server to check its
status.
Start via claude --debug to see more logging.
See Claude Code MCP documentation for further information.
By default, Claude Code asks for confirmation before running MCP tools. When you accept a tool with "Yes, don't ask
again", Claude Code saves that permission to your local .claude/settings.local.json file (which is auto-ignored by
git).
Note: Wildcards do not work for MCP tool permissions. Each tool must be approved individually or listed explicitly in your settings.
Tool naming: By default, guide.toolPrefix is empty, so MCP tools are exposed with their original names (e.g.,
mcp__embabel__docs_vectorSearch). You can set a custom prefix in your application configuration to namespace your
tools.
See Claude Code Permission Modes for detailed documentation on how permissions work.
Create or update .codex/config.toml and add the following MCP server entry:
[mcp_servers.embabel_guide]
command = "npx"
args = ["-y", "mcp-remote", "http://localhost:1337/sse", "--transport", "sse-only"]
startup_timeout_sec = 60
tool_timeout_sec = 120Cursor MCP config (Linux):
~/.cursor/mcp.json
Example (recommended: use mcp-remote as a stdio bridge for SSE):
{
"mcpServers": {
"embabel-dev": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:1337/sse",
"--transport",
"sse-only"
]
}
}
}If you start the server after Cursor is already running, or if the server was temporarily down, Cursor may not automatically respawn the MCP process. In Cursor:
- Command Palette → Developer: Reload Window
You should then see the MCP server listed with tools enabled:
- Open the MCP store via the "..." dropdown at the top of the editor's agent panel.
- Click on "Manage MCP Servers"
- Click on "View raw config"
- Modify the mcp_config.json with Embabel MCP server configuration.
{
"mcpServers": {
"embabel-dev": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:1337/sse",
"--transport",
"sse-only"
]
}
}
}Follow the official instructions for the troubleshooting. https://antigravity.google/docs/mcp#connecting-custom-mcp-servers
- Modify the $HOME/.copilot/mcp-config.json with Embabel MCP server configuration
{
"mcpServers": {
"embabel-dev": {
"type": "sse",
"url": "http://localhost:1337/sse",
"tools": [
"*"
]
}
}
}The backend supports any client via WebSocket (for real-time chat) and REST (for authentication).
Endpoint: ws://localhost:1337/ws
Uses STOMP protocol over WebSocket with SockJS fallback. Any STOMP client library works (e.g., @stomp/stompjs for
JavaScript, stomp.py for Python).
Authentication: Pass an optional JWT token as a query parameter:
ws://localhost:1337/ws?token=<JWT>
If no token is provided, an anonymous user is created automatically.
| Direction | Destination | Purpose |
|---|---|---|
| Subscribe | /user/queue/messages |
Receive chat responses |
| Subscribe | /user/queue/status |
Receive typing/status updates |
| Publish | /app/chat.sendToJesse |
Send message to AI bot |
| Publish | /app/presence.ping |
Keep-alive (send every 30s) |
Sending a message:
{
"body": "your message here"
}Receiving a message:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"content": "response text",
"userId": "bot:jesse",
"userName": "Jesse",
"timestamp": "2025-12-16T10:30:00Z"
}Receiving a status update:
{
"fromUserId": "bot:jesse",
"status": "typing"
}CORS is open (*), no special headers required beyond Content-Type: application/json.
Register:
POST /api/hub/register
{
"userDisplayName": "Jane Doe",
"username": "jane",
"userEmail": "jane@example.com",
"password": "secret",
"passwordConfirmation": "secret"
}
Login:
POST /api/hub/login
{ "username": "jane", "password": "secret" }
Response:
{ "token": "eyJhbG...", "userId": "...", "username": "jane", ... }
List Personas:
GET /api/hub/personas
Update Persona (requires auth):
PUT /api/hub/persona/mine
Authorization: Bearer <JWT>
{ "persona": "persona_name" }
import {Client} from '@stomp/stompjs';
import SockJS from 'sockjs-client';
const client = new Client({
webSocketFactory: () => new SockJS('http://localhost:1337/ws'),
onConnect: () => {
// Subscribe to responses
client.subscribe('/user/queue/messages', (frame) => {
const message = JSON.parse(frame.body);
console.log('Received:', message.content);
});
// Send a message
client.publish({
destination: '/app/chat.sendToJesse',
body: JSON.stringify({body: 'Hello!'})
});
}
});
client.activate();For a fully-fledged client, see hub.embabel.com.
Start neo4j + guide (the Java application):
docker compose --profile java up --build -dThe Dockerfile uses a multi-stage build that compiles the application from source inside the container. This means:
- ✅ Works from a fresh clone (no Java/Maven installation required locally)
- ✅ Only Docker is needed to build and run
⚠️ First build takes ~2-3 minutes (Maven compilation inside Docker)
The build process:
- Stage 1: Uses
maven:3.9-eclipse-temurin-25to compile the application - Stage 2: Uses lightweight
eclipse-temurin:25-jre-jammyruntime image with the compiled JAR
This approach ensures consistency across environments and simplifies onboarding for new contributors.
If you're running the Java application locally (e.g., from your IDE), you can start only Neo4j:
COMPOSE_PROFILES= docker compose up -dOr equivalently:
docker compose up neo4j -dThis is useful during development when you want faster iteration with your local Java process.
If port 1337 is already in use (for example, the chatbot app is running), override the exposed port:
GUIDE_PORT=1338 docker compose --profile java up --build -dThis maps container port 1337 → host port 1338, so MCP SSE becomes:
http://localhost:1338/sse
Docker Compose supports environment variable overrides. You can set them inline (shown below) or put them in a local
.env file next to compose.yaml (Docker Compose auto-loads it).
GUIDE_PORT: override host port mapping (default1337)OPENAI_API_KEY: required for LLM callsNEO4J_VERSION/NEO4J_USERNAME/NEO4J_PASSWORD: Neo4j settings (optional)DISCORD_TOKEN: optional, to enable the Discord bot
For local/MCP use, the guide container needs at least one LLM provider key. Supported providers (in auto-detection order): OPENAI_API_KEY, ANTHROPIC_API_KEY, MISTRAL_API_KEY, DEEPSEEK_API_KEY.
For hub/web deployments, no server-side key is needed — users bring their own via Settings → Integrations.
- Create a
.envfile next tocompose.yaml:
OPENAI_API_KEY=sk-your-key-here- Or pass it inline:
OPENAI_API_KEY=sk-... docker compose --profile java up --build -dPORT=${GUIDE_PORT:-1337}
curl -i --max-time 3 "http://localhost:${PORT}/sse"You should see Content-Type: text/event-stream and an event:endpoint line.
docker compose --profile java down --remove-orphans| Variable | Default | Description |
|---|---|---|
COMPOSE_PROFILES |
java |
Set to empty to run Neo4j only (no Java service) |
NEO4J_VERSION |
2025.10.1-community-bullseye |
Neo4j Docker image tag |
NEO4J_USERNAME |
neo4j |
Neo4j username |
NEO4J_PASSWORD |
brahmsian |
Neo4j password |
NEO4J_HTTP_PORT |
7474 |
Neo4j HTTP port |
NEO4J_BOLT_PORT |
7687 |
Neo4j Bolt port |
NEO4J_HTTPS_PORT |
7473 |
Neo4j HTTPS port |
OPENAI_API_KEY |
(optional) | OpenAI API key (or any one provider key below) |
ANTHROPIC_API_KEY |
(optional) | Anthropic API key |
MISTRAL_API_KEY |
(optional) | Mistral API key |
DEEPSEEK_API_KEY |
(optional) | DeepSeek API key |
EMBABEL_KEY_SECRET |
(recommended) | AES key for BYOK key encryption (openssl rand -base64 32) |
DISCORD_TOKEN |
(optional) | Discord bot token |
Example:
NEO4J_PASSWORD=mysecretpassword OPENAI_API_KEY=sk-... GUIDE_PORT=1338 docker compose --profile java up --build -dTests require the following:
- LLM API Key: Set at least one provider key in your environment before running tests:
export OPENAI_API_KEY=sk-your-key-here-
Neo4j: See the Local vs CI Testing section below.
-
A JDK between 21 and 25. The build targets Java 21 bytecode, so the resulting jar runs on any JRE 21+, but it builds on anything up to 25 — CI runs the suite on both 21 and 25. If you raise
<java.version>in the pom, note that Kotlin must be 2.3.0+ to target 25 at all, and thecodegen-gradleKotlin version has to move with it.
The test suite uses Neo4j, which can be provided in two ways:
| Mode | USE_LOCAL_NEO4J |
How Neo4j is provided | Best for |
|---|---|---|---|
| CI (default) | unset/false |
Testcontainers spins up Neo4j automatically | GitHub Actions, fresh environments |
| Local development | true |
You run Neo4j via Docker Compose | Faster iteration |
For faster test runs during development, use a local Neo4j instance:
- Start Neo4j:
docker compose up neo4j -d- Run tests with
USE_LOCAL_NEO4J=true:
export OPENAI_API_KEY=sk-your-key-here
USE_LOCAL_NEO4J=true ./mvnw testOr add to your shell profile for persistence:
export USE_LOCAL_NEO4J=trueLeave USE_LOCAL_NEO4J unset (the default). GitHub Actions uses Testcontainers to automatically spin up Neo4j.
./mvnw testAll tests should pass, including:
- Hub API controller tests
- User service tests
- Neo4j repository tests
- MCP Security regression tests (verifies
/sseand/mcpendpoints are not blocked by Spring Security)
Sometimes (for example if your IDE crashes) you will be left with an orphaned server process and won't be able to restart. To kill the server:
lsof -ti:1337 | xargs kill -9
