openHAB MCP Server + Ollama + Open WebUI — Complete Setup Guide

openHAB MCP Server + Ollama + Open WebUI — Complete Setup Guide


Hardware Recommendations

To run a capable LLM model alongside openHAB MCP and Open WebUI, the following hardware is recommended:

Minimum (CPU-only, small models only)

  • CPU: Modern x86_64 with AVX2 support, 8+ cores on a single NUMA node
  • RAM: 16 GB
  • Storage: 50 GB SSD
  • OS: Debian 12 / Ubuntu 22.04+
  • Suitable models: qwen2.5:3b, llama3.2:3b

Recommended (GPU-accelerated, full capability)

  • CPU: Any modern 8-core+ CPU (Intel/AMD)
  • RAM: 32 GB
  • GPU: NVIDIA GPU with 12+ GB VRAM (e.g. RTX 3060 12GB, RTX 4070, RTX 3090)
    • CUDA 12.x required
    • PCIe passthrough to LXC/VM if running on Proxmox
  • Storage: 100 GB SSD
  • OS: Debian 12 / Ubuntu 22.04+ (bare metal, VM, or Proxmox LXC with GPU passthrough)
  • Suitable models: qwen2.5:14b, llama3.3:70b-instruct-q4_K_M, mistral-small:22b

Note: Without a GPU, stick to 3B–7B quantized models (Q4). With a GPU, models like
qwen2.5:14b or llama3.3:70b-q4 deliver significantly better instruction-following,
tool-calling accuracy, and reasoning quality — highly recommended for reliable openHAB
automation via MCP.


Architecture Overview

openHAB (REST API + SSE)
        ↓
openhab-mcp  (Python MCP server, port 8081)
        ↓
mcpo         (MCP → OpenAPI proxy, port 8000)
        ↓
Open WebUI   (connects to mcpo as Tool Server)
        ↓
Ollama       (LLM backend, port 11434)

Part 1 — openHAB MCP Server

1.1 Prerequisites

apt update && apt install -y git python3 python3-pip python3-venv

1.2 Clone the Repository

git clone https://github.com/DrRSatzteil/openhab-mcp.git /opt/openhab-mcp
cd /opt/openhab-mcp

1.3 Create Python Virtual Environment

python3 -m venv .
source bin/activate
pip install -e .
deactivate

1.4 Configure Environment

Create the configuration file:

nano /opt/openhab-mcp/.env

Add the following content (adjust to your openHAB instance):

OPENHAB_URL=http://192.168.0.196:8080
# Uncomment and set if openHAB requires authentication:
# OPENHAB_API_TOKEN=your_api_token_here
# Or basic auth:
# OPENHAB_USERNAME=admin
# OPENHAB_PASSWORD=yourpassword

# SSE transport settings
MCP_TRANSPORT=sse
MCP_PORT=8081
MCP_HOST=0.0.0.0

1.5 Create systemd Service

nano /etc/systemd/system/openhab-mcp.service
[Unit]
Description=openHAB MCP Server
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/openhab-mcp
EnvironmentFile=/opt/openhab-mcp/.env
ExecStart=/opt/openhab-mcp/bin/python -m openhab_mcp
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now openhab-mcp.service
systemctl status openhab-mcp.service

Verify the SSE endpoint is reachable:

curl -s http://localhost:8081/sse

Part 2 — Ollama

2.1 Install Ollama

curl -fsSL https://ollama.com/install.sh | sh

2.2 Configure the systemd Service

Edit the Ollama service to expose it on all interfaces and tune performance:

nano /etc/systemd/system/ollama.service
[Unit]
Description=Ollama Service
After=network-online.target

[Service]
Environment="OLLAMA_HOST=0.0.0.0"
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Performance tuning
Environment="OLLAMA_NUM_PARALLEL=1"
Environment="OLLAMA_NUM_CTX=4096"
# Set to the number of CPU cores assigned to this host/container
Environment="OLLAMA_NUM_THREAD=8"
# Uncomment to keep model loaded permanently (avoids reload delay)
# Environment="OLLAMA_KEEP_ALIVE=-1"

ExecStart=/usr/local/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3

[Install]
WantedBy=default.target
systemctl daemon-reload
systemctl enable --now ollama.service

2.3 Pull the Recommended Model

With GPU (recommended):

# Best balance of quality and speed for tool-calling / function use
ollama pull qwen2.5:14b

# If you have 24+ GB VRAM, consider:
# ollama pull llama3.3:70b-instruct-q4_K_M
# ollama pull mistral-small:22b

Without GPU (CPU-only fallback):

# Lightweight models suitable for CPU inference
ollama pull qwen2.5:3b
# or
ollama pull llama3.2:3b

Note: For reliable MCP tool-calling, models with 7B+ parameters and instruction-tuned
variants (tagged :instruct) are strongly recommended. Smaller models (3B) can work but
may produce inconsistent tool invocations.

Verify Ollama is working:

ollama run qwen2.5:14b "Hello, respond with one sentence."

Part 3 — Open WebUI

3.1 Install Open WebUI

apt install -y python3-venv
mkdir -p /opt/open-webui
cd /opt/open-webui
python3 -m venv .
source bin/activate
pip install open-webui
deactivate

3.2 Create systemd Service

nano /etc/systemd/system/open-webui.service
[Unit]
Description=Open WebUI
After=network.target ollama.service

[Service]
Type=simple
WorkingDirectory=/opt/open-webui
Environment="PATH=/opt/open-webui/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Environment="OLLAMA_BASE_URL=http://127.0.0.1:11434"
Environment="WEBUI_SECRET_KEY=change_this_to_a_random_secret"
ExecStart=/opt/open-webui/bin/open-webui serve --host 0.0.0.0 --port 3000
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now open-webui.service
systemctl status open-webui.service

Open WebUI is now accessible at http://<your-host>:3000.


Part 4 — mcpo (MCP → OpenAPI Proxy)

4.1 Install Node.js

curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
node -v && npx --version

4.2 Install mcpo into Open WebUI venv

cd /opt/open-webui
source bin/activate
pip install mcpo
deactivate

4.3 Create systemd Service

nano /etc/systemd/system/mcpo-openhab.service
[Unit]
Description=MCPO Proxy for openHAB MCP
After=network.target openhab-mcp.service

[Service]
Type=simple
WorkingDirectory=/opt/open-webui
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ExecStart=/opt/open-webui/bin/mcpo --port 8000 -- /usr/bin/npx mcp-remote http://127.0.0.1:8081/sse --allow-http
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Note: If openHAB MCP runs on a different host, replace 127.0.0.1:8081 with the
actual IP address, e.g. http://192.168.0.196:8081.

systemctl daemon-reload
systemctl enable --now mcpo-openhab.service
systemctl status mcpo-openhab.service

Verify the OpenAPI endpoint is available:

curl -s http://localhost:8000/openapi.json | python3 -m json.tool | head -30

You should see a JSON document listing all 65 openHAB MCP tools as API endpoints.


Part 5 — Connect MCP Server in Open WebUI

Follow these steps to register the openHAB tool server in the Open WebUI admin area:

Step 1: Open Open WebUI in your browser:

http://<your-host>:3000

Step 2: Log in as an admin user. If this is a fresh installation, the first registered
account becomes the admin.

Step 3: Click your profile icon in the bottom-left corner, then click Admin Panel.

Step 4: In the Admin Panel, navigate to SettingsTools.

Step 5: Click the + button to add a new tool server.

Step 6: In the URL field, enter:

http://localhost:8000

Leave the API key field empty (unless you configured one in mcpo).

Step 7: Click Save. Open WebUI will fetch the OpenAPI spec from mcpo and import all
tools automatically. You should see all openHAB tools listed.

Step 8: To use the tools in a chat, open a new conversation and:

  • Select a model that supports tool-calling (e.g. qwen2.5:14b)
  • Click the Tools icon (wrench symbol) in the chat input bar
  • Enable the openHAB tools from the list

Step 9: Test with a prompt such as:

What is the current state of my lights in the living room?

The model should call the appropriate openHAB MCP tool and return the result.


Service Overview

Service Port Start Command
openhab-mcp 8081 systemctl start openhab-mcp
ollama 11434 systemctl start ollama
open-webui 3000 systemctl start open-webui
mcpo-openhab 8000 systemctl start mcpo-openhab

All services are enabled to start automatically on boot.


Troubleshooting

mcpo fails to start (npx not found):
Make sure the PATH in the service unit includes the Node.js binary directory:

which npx   # e.g. /usr/bin/npx

Update the Environment="PATH=..." line in the service accordingly.

mcpo fails with Non-HTTPS URLs are only allowed for localhost:
Add --allow-http to the ExecStart line in mcpo-openhab.service (already included above).

Open WebUI cannot reach Ollama:
Verify the OLLAMA_BASE_URL in the Open WebUI service and check that Ollama binds to
0.0.0.0 (set via OLLAMA_HOST=0.0.0.0).

Model does not call tools:
Ensure the selected model supports function/tool calling. In Ollama, models with tool support
are marked in their manifest. Recommended: qwen2.5:14b, llama3.1:8b, mistral:7b-instruct.

Hello,

I’m wondering why you use mcpo, as Open WebUI can use MCP directly ?

Thank you for the tutorial! Though I need to note that as of at least OH 5.2 M6 there is an MCP add-on. openHAB MCP Server - System Integrations | openHAB I imagine this could simplify things a bit?

I keep wondering why the documentation suggests to send a bearer token for every request though? Afaik and also in my actual experience this won’t work reliably on all endpoints (at least with a reverse proxy setup). Shouldn’t it be a X-OPENHAB-TOKEN header instead? But of course this might work for the specific mcp endpoint, I haven’t tested this new addon yet

Thanks for the tutorial, it was very useful!

I used the MCP add-on mentioned by rlkoshak. The add-on does not need any configuration in openHAB (default settings are OK), but you need to generate a login token. There is then a modification required to the part 4.3 of the tutorial: the address will be 127.0.0.1:8080/mcp and you need to append the token: --header Authorization: Bearer oh.XXX.YYYY

So far I have been able to access openHAB via the mcpo server successfully using LM Studio (running the LLM in LM Studio) or Open WebUI + llama.cpp (instead of Ollama). For some reason I can’t get Ollama to execute OpenAPI tools :thinking:

I really don’t understand why mcpo is needed ?
1- openHAB already has a direct OpenAPI exposition. you should be able to connect directly from Open WebUI to it. Unless you really want to use the tool exposition created by openhab-mcp. But in this case…
2- …Open WebUI can also directly connect to a mcp server like the one defined in the OP, no need for an OpenAPI proxy like mcpo.

But as Rich mentionned, there is now an official MCP server integration in OpenHAB. You can completely bypass openhab-mcp and mcpo and connect directly Open WebUI to OpenHAB MCP. I use this and it works.

Ollama won’t execute OpenAPI tools or MCP tools. It is not a tool/MCP client. Its purpose is to “serve” a LLM endpoint.
You use a dedicated client, like Open WebUI, or a coding agent, for this. They “glue” the LLM with externality (like yourself, or others tools/services)

Firstly, what I mean by Ollama not executing tools, is this:

  1. in Open WebUI Admin Panel → Settings → Connections → under Ollama API there is the connection to my Ollama instance. A prompt for any model using this connection does not call the openhab-mcp tools
  2. in Open WebUI Admin Panel → Settings → Connections → under OpenAI API there is the connection to my llama.cpp instance. A prompt for a model using this connection does call the openhab-mcp tools
    Both Ollama and llama.cpp are serving the Llama 3.2 3B Instruct model (I’ve tried others as well, but this one is currently loaded in the llama.cpp instance). For me it is OK to just use the llama.cpp.

The second thing; indeed, sounds like the MCP add-on + MCPO is not needed as an OpenAPI compatible API is already available in openHAB :thinking: but for the sake of AI and MCP - indeed too, Open WebUI can connect to MCP servers natively! The documentation is outdated (tries to lead to External tools page that does not exist) and the UI is misleading at least to me (it mentions OpenAPI only). The trick is to go to Admin Panel → Settings → Integrations → click the + sign at External Tool Servers → click “OpenAPI” to change the type → fill the URL (ex. 127.0.0.1:8080/mcp) and Bearer API Key (the token). I did not realize that the type could be changed by clicking the “OpenAPI” label :thinking:

I’ll add that I tried to get Open WebUI to “talk to” the official MCP add-on yesterday, and failed. I must say that the way you configure these things in Open WebUI is some of the most messy thing I’ve ever seen. It’s really, really confusing, and the permission/access setting is especially so.

But, I think there are some pretty basic bugs in the MCP add-on as well. I ended up trying to use Postman to query the MCP add-on directly, because nothing would happen in Open WebUI (the MCP would show as “green” but be unavailable for use in chat sessions).

I tried several JSONs, and they all got the same response. Here is one of the request JSONs I sent:

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "Postman-Test",
      "version": "1.0.0"
    }
  },
  "id": 1
}

The answer I get depends on what I put in the Accept header, if I use text/event-stream, I get:

{
    "cause": null,
    "stackTrace": [
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "build",
            "fileName": "McpError.java",
            "lineNumber": 72,
            "className": "io.modelcontextprotocol.spec.McpError$Builder",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "responseError",
            "fileName": "JavaxStreamableServerTransportProvider.java",
            "lineNumber": 491,
            "className": "org.openhab.io.mcp.internal.servlet.JavaxStreamableServerTransportProvider",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doPost",
            "fileName": "JavaxStreamableServerTransportProvider.java",
            "lineNumber": 345,
            "className": "org.openhab.io.mcp.internal.servlet.JavaxStreamableServerTransportProvider",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "service",
            "fileName": "HttpServlet.java",
            "lineNumber": 681,
            "className": "javax.servlet.http.HttpServlet",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "service",
            "fileName": "HttpServlet.java",
            "lineNumber": 764,
            "className": "javax.servlet.http.HttpServlet",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "service",
            "fileName": "OsgiInitializedServlet.java",
            "lineNumber": 102,
            "className": "org.ops4j.pax.web.service.spi.servlet.OsgiInitializedServlet",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ServletHolder.java",
            "lineNumber": 799,
            "className": "org.eclipse.jetty.servlet.ServletHolder",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doFilter",
            "fileName": "ServletHandler.java",
            "lineNumber": 1656,
            "className": "org.eclipse.jetty.servlet.ServletHandler$ChainEnd",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doFilter",
            "fileName": "OsgiFilterChain.java",
            "lineNumber": 113,
            "className": "org.ops4j.pax.web.service.spi.servlet.OsgiFilterChain",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doHandle",
            "fileName": "PaxWebServletHandler.java",
            "lineNumber": 334,
            "className": "org.ops4j.pax.web.service.jetty.internal.PaxWebServletHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 143,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "SecurityHandler.java",
            "lineNumber": 600,
            "className": "org.eclipse.jetty.security.SecurityHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "HandlerWrapper.java",
            "lineNumber": 127,
            "className": "org.eclipse.jetty.server.handler.HandlerWrapper",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextHandle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 235,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doHandle",
            "fileName": "SessionHandler.java",
            "lineNumber": 1624,
            "className": "org.eclipse.jetty.server.session.SessionHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextHandle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 233,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doHandle",
            "fileName": "ContextHandler.java",
            "lineNumber": 1440,
            "className": "org.eclipse.jetty.server.handler.ContextHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextScope",
            "fileName": "ScopedHandler.java",
            "lineNumber": 188,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doScope",
            "fileName": "ServletHandler.java",
            "lineNumber": 505,
            "className": "org.eclipse.jetty.servlet.ServletHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doScope",
            "fileName": "SessionHandler.java",
            "lineNumber": 1594,
            "className": "org.eclipse.jetty.server.session.SessionHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextScope",
            "fileName": "ScopedHandler.java",
            "lineNumber": 186,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doScope",
            "fileName": "ContextHandler.java",
            "lineNumber": 1355,
            "className": "org.eclipse.jetty.server.handler.ContextHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 141,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ContextHandlerCollection.java",
            "lineNumber": 234,
            "className": "org.eclipse.jetty.server.handler.ContextHandlerCollection",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "PrioritizedHandlerCollection.java",
            "lineNumber": 96,
            "className": "org.ops4j.pax.web.service.jetty.internal.PrioritizedHandlerCollection",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "HandlerWrapper.java",
            "lineNumber": 127,
            "className": "org.eclipse.jetty.server.handler.HandlerWrapper",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "Server.java",
            "lineNumber": 516,
            "className": "org.eclipse.jetty.server.Server",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "lambda$handle$1",
            "fileName": "HttpChannel.java",
            "lineNumber": 487,
            "className": "org.eclipse.jetty.server.HttpChannel",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "dispatch",
            "fileName": "HttpChannel.java",
            "lineNumber": 732,
            "className": "org.eclipse.jetty.server.HttpChannel",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "HttpChannel.java",
            "lineNumber": 479,
            "className": "org.eclipse.jetty.server.HttpChannel",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "onFillable",
            "fileName": "HttpConnection.java",
            "lineNumber": 277,
            "className": "org.eclipse.jetty.server.HttpConnection",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "succeeded",
            "fileName": "AbstractConnection.java",
            "lineNumber": 311,
            "className": "org.eclipse.jetty.io.AbstractConnection$ReadCallback",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "fillable",
            "fileName": "FillInterest.java",
            "lineNumber": 105,
            "className": "org.eclipse.jetty.io.FillInterest",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "ChannelEndPoint.java",
            "lineNumber": 104,
            "className": "org.eclipse.jetty.io.ChannelEndPoint$1",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "runTask",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 338,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doProduce",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 315,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "tryProduce",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 173,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 131,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "ReservedThreadExecutor.java",
            "lineNumber": 409,
            "className": "org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "runJob",
            "fileName": "QueuedThreadPool.java",
            "lineNumber": 883,
            "className": "org.eclipse.jetty.util.thread.QueuedThreadPool",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "QueuedThreadPool.java",
            "lineNumber": 1034,
            "className": "org.eclipse.jetty.util.thread.QueuedThreadPool$Runner",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": "java.base",
            "moduleVersion": "21.0.5",
            "methodName": "run",
            "fileName": "Thread.java",
            "lineNumber": 1583,
            "className": "java.lang.Thread",
            "nativeMethod": false
        }
    ],
    "jsonRpcError": {
        "code": -32603,
        "message": "application/json required in Accept header"
    },
    "message": "application/json required in Accept header",
    "suppressed": [],
    "localizedMessage": "application/json required in Accept header"
}

…and if I use application/json, I get:

{
    "cause": null,
    "stackTrace": [
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "build",
            "fileName": "McpError.java",
            "lineNumber": 72,
            "className": "io.modelcontextprotocol.spec.McpError$Builder",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "responseError",
            "fileName": "JavaxStreamableServerTransportProvider.java",
            "lineNumber": 491,
            "className": "org.openhab.io.mcp.internal.servlet.JavaxStreamableServerTransportProvider",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doPost",
            "fileName": "JavaxStreamableServerTransportProvider.java",
            "lineNumber": 345,
            "className": "org.openhab.io.mcp.internal.servlet.JavaxStreamableServerTransportProvider",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "service",
            "fileName": "HttpServlet.java",
            "lineNumber": 681,
            "className": "javax.servlet.http.HttpServlet",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "service",
            "fileName": "HttpServlet.java",
            "lineNumber": 764,
            "className": "javax.servlet.http.HttpServlet",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "service",
            "fileName": "OsgiInitializedServlet.java",
            "lineNumber": 102,
            "className": "org.ops4j.pax.web.service.spi.servlet.OsgiInitializedServlet",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ServletHolder.java",
            "lineNumber": 799,
            "className": "org.eclipse.jetty.servlet.ServletHolder",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doFilter",
            "fileName": "ServletHandler.java",
            "lineNumber": 1656,
            "className": "org.eclipse.jetty.servlet.ServletHandler$ChainEnd",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doFilter",
            "fileName": "OsgiFilterChain.java",
            "lineNumber": 113,
            "className": "org.ops4j.pax.web.service.spi.servlet.OsgiFilterChain",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doHandle",
            "fileName": "PaxWebServletHandler.java",
            "lineNumber": 334,
            "className": "org.ops4j.pax.web.service.jetty.internal.PaxWebServletHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 143,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "SecurityHandler.java",
            "lineNumber": 600,
            "className": "org.eclipse.jetty.security.SecurityHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "HandlerWrapper.java",
            "lineNumber": 127,
            "className": "org.eclipse.jetty.server.handler.HandlerWrapper",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextHandle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 235,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doHandle",
            "fileName": "SessionHandler.java",
            "lineNumber": 1624,
            "className": "org.eclipse.jetty.server.session.SessionHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextHandle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 233,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doHandle",
            "fileName": "ContextHandler.java",
            "lineNumber": 1440,
            "className": "org.eclipse.jetty.server.handler.ContextHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextScope",
            "fileName": "ScopedHandler.java",
            "lineNumber": 188,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doScope",
            "fileName": "ServletHandler.java",
            "lineNumber": 505,
            "className": "org.eclipse.jetty.servlet.ServletHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doScope",
            "fileName": "SessionHandler.java",
            "lineNumber": 1594,
            "className": "org.eclipse.jetty.server.session.SessionHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "nextScope",
            "fileName": "ScopedHandler.java",
            "lineNumber": 186,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doScope",
            "fileName": "ContextHandler.java",
            "lineNumber": 1355,
            "className": "org.eclipse.jetty.server.handler.ContextHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ScopedHandler.java",
            "lineNumber": 141,
            "className": "org.eclipse.jetty.server.handler.ScopedHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "ContextHandlerCollection.java",
            "lineNumber": 234,
            "className": "org.eclipse.jetty.server.handler.ContextHandlerCollection",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "PrioritizedHandlerCollection.java",
            "lineNumber": 96,
            "className": "org.ops4j.pax.web.service.jetty.internal.PrioritizedHandlerCollection",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "HandlerWrapper.java",
            "lineNumber": 127,
            "className": "org.eclipse.jetty.server.handler.HandlerWrapper",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "Server.java",
            "lineNumber": 516,
            "className": "org.eclipse.jetty.server.Server",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "lambda$handle$1",
            "fileName": "HttpChannel.java",
            "lineNumber": 487,
            "className": "org.eclipse.jetty.server.HttpChannel",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "dispatch",
            "fileName": "HttpChannel.java",
            "lineNumber": 732,
            "className": "org.eclipse.jetty.server.HttpChannel",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "handle",
            "fileName": "HttpChannel.java",
            "lineNumber": 479,
            "className": "org.eclipse.jetty.server.HttpChannel",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "onFillable",
            "fileName": "HttpConnection.java",
            "lineNumber": 277,
            "className": "org.eclipse.jetty.server.HttpConnection",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "succeeded",
            "fileName": "AbstractConnection.java",
            "lineNumber": 311,
            "className": "org.eclipse.jetty.io.AbstractConnection$ReadCallback",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "fillable",
            "fileName": "FillInterest.java",
            "lineNumber": 105,
            "className": "org.eclipse.jetty.io.FillInterest",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "ChannelEndPoint.java",
            "lineNumber": 104,
            "className": "org.eclipse.jetty.io.ChannelEndPoint$1",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "runTask",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 338,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "doProduce",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 315,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "tryProduce",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 173,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "EatWhatYouKill.java",
            "lineNumber": 131,
            "className": "org.eclipse.jetty.util.thread.strategy.EatWhatYouKill",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "ReservedThreadExecutor.java",
            "lineNumber": 409,
            "className": "org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "runJob",
            "fileName": "QueuedThreadPool.java",
            "lineNumber": 883,
            "className": "org.eclipse.jetty.util.thread.QueuedThreadPool",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "run",
            "fileName": "QueuedThreadPool.java",
            "lineNumber": 1034,
            "className": "org.eclipse.jetty.util.thread.QueuedThreadPool$Runner",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": "java.base",
            "moduleVersion": "21.0.5",
            "methodName": "run",
            "fileName": "Thread.java",
            "lineNumber": 1583,
            "className": "java.lang.Thread",
            "nativeMethod": false
        }
    ],
    "jsonRpcError": {
        "code": -32603,
        "message": "text/event-stream required in Accept header"
    },
    "message": "text/event-stream required in Accept header",
    "suppressed": [],
    "localizedMessage": "text/event-stream required in Accept header"
}

This looks pretty broken to me, even if the request is invalid in some way, why convert a whole stack trace to JSON and dump that as a response? And, why require that the Accept header is two things at once?

I can partially confirm your observation with Open WebUI.

  • Using a good/large external model with an OpenAI API, when asking “List all tools you have access to”, I get a good listing of the tools available, including all OpenHAB MCP tools.
  • When using an ollama model, the same request give me really bad results.

I think that the model size / quality is the culprit here. I tested several ollama models and they give me very different results. It could be tied to the context size, or that the tool calling precision drops when too many tools are available.

For example, when Open WebUI internal tools are enabled:

  • the best ollama models I can use shows me one or two OpenHAB MCP tools, and pretty much every internal Open WebUI tools.
  • llama 3.2:3b is so bad that I can hardly have one tool listed, usually one of the Open WebUI internal one.

When Open WebUI internal tools are disabled:

  • the best ollama models I can use shows me several OpenHAB MCP tools.
  • llama 3.2:3b shows me, sometimes, an openHAB one. Or fails miserabily.

I think that the model capability is the culprit, but the fact that you got better result with llama.cpp with the same model is really confusing.

BTW, if you can run it, I strongly encourage you to test the ollama model ssfdre38/gemma4-turbo:e2b (but you MUST disable thinking if you don’t want to wait). Results are pretty good and fast for a model this size. I use it with the HLI provided by the beta/marketplace ChatGPT binding, linked to OpenWebUI API as an intermediary (to disable thinking). Tool calling with speed is HARD for small models, and the gemma 4 model is the first one I try which is able to do it reliably. I get less than two seconds delay when asking to light a bulb (I use a very old RTX 1060)

I couldn’t agree more…

Did you activate it in the chat session ?

I never found it, possibly because of their “unique UIX”, but I managed to find and enable it now.

Now I have that other issue that I’ve had before, where there is some miscommunication between Open WebUI and Qwen, so that it just answers by sending JSONs. I don’t know exactly what it’s about, but I’ve experiences in the past that it can “suddenly fix itself” and it starts outputting actualy replies. The whole thing is very much “experimental” I would say:

This is just immensely useful, but I don’t think it’s related to OH or the MCP, but some communication issue between Open WebUI and Qwen, as I’ve had that happen before without the MCP being involved. I guess I’ll have to try another model, because I can’t figure out why it happens:

Tool calling with a LLM is completely a demonic hack. (demonic as “how the hell can something like this really work at all??”). But a hack that can do incredible thing nonetheless.
Many small models mess this up, and do not put the correct JSON markup to do ‘proper’ tool calling. Then JSON garbage ends up in the conversation, like the one you get. But it’s surprising, because the qwen model you use (32b) should be pretty good at this.

It’s the “beauty” of the things, there is nothing to understand :rofl:. The LLM just fails and outputs garbage.

Yes, it’s not like I’m trying to use a 3b model..

Just as a test, I switched to llama3.1:8b-instruct-q8_0, and then I actually get a response, not just JSONs..

Based on the tool call response, it appears that there are several issues with your openHAB MCP setup.

  1. Devices offline: There are 12 devices that are currently offline, including a Kodi media player, a digital media server, and an Ecobee thermostat. You should investigate why these devices are not connecting to openHAB.
  2. Device registration issues: Some of the offline devices have registration issues, indicated by the message “@text/offline.device-not-registered”. This suggests that there may be problems with device discovery or communication between openHAB and the devices.
  3. Connection issues: There is also a mention of a connection issue with an Ntfy server, which is not responding as expected.

To troubleshoot these issues, you should:

  1. Check your network connections and ensure that all devices are properly connected to your home network.
  2. Investigate device registration issues by reviewing the openHAB logs for any errors related to device discovery or communication.
  3. Look into the Ntfy server connection issue and try restarting it if necessary.

By addressing these issues, you should be able to resolve the problems with your openHAB MCP setup.

So, I guess that Open WebUI and Qwen just aren’t friends. I’ll have to try to find a more suitable model I guess.

PS! This is just a test installation of OH that I’ve used to debug all kind of bugs and problems, I have bindings installed and configured for devices and services that don’t exist, so the “problems” reported are of no interest in themselves - but the fact that the problems are reported shows that at least something is working.

To get a more capable model that isn’t Qwen, I’ve downloaded mistral-small. It, too, can talk to the MCP, but I’m wondering if it has something of a “God complex”:

I really hope I don’t accidentally make it turn off the sun!

The usual “AI slop” is prevalent. I asked it a stupid question, but it was one of the “suggested follow-ups”.

Is there a way to automatically restart or reconnect these offline devices?

The answer is that there’s nothing it can do to reconnect these, most of them don’t even exist (they are just Things used in previous tests). But, instead, it goes on providing utter BS suggestions:

To automatically restart or reconnect offline devices in openHAB, you can use rules and scripts. Here are some general steps to achieve this:

  1. Create Rules for Automatic Reconnection:

    • You can create rules in openHAB that monitor the status of your things and attempt to reconnect them if they go offline.
    • Use the ThingStatusInfo channel to check the status of your things.
  2. Example Rule for Reconnecting a Thing:

    rule "Reconnect Kodi (Lappo) Renderer"
    when
        Item Kodi_Lappo changed to OFFLINE
    then
        // Attempt to reconnect or restart the thing
        sendCommand(Kodi_Lappo, ON)
    end
    
  3. Use Exec Binding for Restarting Devices:

    • If the device can be restarted via a command (e.g., SSH), you can use the Exec binding to execute the necessary commands.
    rule "Restart Kodi (Lappo) Renderer"
    when
        Item Kodi_Lappo changed to OFFLINE
    then
        // Execute a command to restart the device
        sendCommand(Exec_Restart_Kodi, ON)
    end
    
  4. Check Configuration and Credentials:

    • Ensure that the configuration parameters for each thing are correct.
    • For the Ecobee account, make sure the API key is valid and the authorization is complete.
  5. Monitoring and Logging:

    • Add logging to your rules to monitor the reconnection attempts and their outcomes.
    rule "Log Reconnection Attempts"
    when
        Item Kodi_Lappo changed to OFFLINE
    then
        logInfo("Reconnection", "Attempting to reconnect Kodi (Lappo) Renderer")
    end
    
  6. Example Rule for Ntfy Server:

    rule "Reconnect Ntfy Server"
    when
        Item Ntfy_Connection changed to OFFLINE
    then
        // Attempt to reconnect or restart the thing
        sendCommand(Ntfy_Connection, ON)
    end
    
  7. Example Rule for Ecobee Account:

    rule "Reconnect Ecobee Account"
    when
        Item Ecobee_Account changed to OFFLINE
    then
        // Attempt to reconnect or restart the thing
        sendCommand(Ecobee_Account, ON)
    end
    
  8. Example Rule for BubbleUPnP Renderer:

    rule "Reconnect BubbleUPnP Renderer"
    when
        Item BubbleUPnP_S62_Pro changed to OFFLINE
    then
        // Attempt to reconnect or restart the thing
        sendCommand(BubbleUPnP_S62_Pro, ON)
    end
    

By implementing these rules and scripts, you can automate the process of reconnecting offline devices in your openHAB system. If you need more specific guidance or encounter issues, feel free to ask!