AIAI Coding AgentsLLM

MCP Tools for Local LLMs: Connect MCP Servers to Open WebUI with mcpo (Linux, Windows, Mac)

Before you start

  • 17 min read
  • 14 to run in terminal
  • 4 file contents

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

MCP lets a chat app hand your local model tools - the current time, a web page fetcher, a folder it may read - and mcpo is the small proxy that makes command-line MCP servers work in Open WebUI. Your model asks for a tool, Open WebUI runs it, and the result goes back to the model. Nothing leaves your computer except what a tool itself fetches.

Why bother? A local model only knows its training data. With tools it can check today's date, read a page you point it at, or look through a folder you share on purpose. It is the same idea agents like Claude Code use, now in your own chat window.

The short version of the choice:

  • Server already speaks Streamable HTTP? Add it directly. Open WebUI has native MCP support for exactly that.
  • Server is a command you start (uvx, npx)? Put mcpo in front of it. It turns the server into a normal OpenAPI tool server.
Open WebUIv0.12.4, released 2026-10-25, image ghcr.io/open-webui/open-webui:v0.12.4
mcpov0.0.20, released 2026-02-27 (no newer release listed). Official image ghcr.io/open-webui/mcpo:main - there is no v0.0.20 image tag, so we pin a digest (Step 2)
Time neededAbout 20 minutes plus the model download
RAMOpen WebUI alone idles near 300 MiB according to the official docs (a 0.12.3 slim-image test). That excludes the model, the Docker VM and tool servers. We did not measure the full stack.
Ports3000 (Open WebUI, bound to 127.0.0.1). mcpo (8000) and Ollama (11434) are not published.
Official docsdocs.openwebui.com MCP page, open-webui/mcpo

Which version, and why (checked 2026-10-26). Open WebUI v0.12.4 is one day old. We found no repeated reports of failed installs or data loss, and it contains security fixes, so this guide uses it. We cannot call it long-proven, so back up before you run it. mcpo is different: issue #303 is still open and several users confirm that a fresh uvx mcpo pulls MCP SDK 2.x and fails at import. mcpo v0.0.19 is not a verified good fallback, so instead of an old release this guide constrains the SDK with mcp<2.

Already running Open WebUI with Ollama? This guide builds on the same idea as our older Open WebUI with Ollama and Docker Compose guide. You can add the tool parts to an existing setup.

How it fits together#

You chat in Open WebUI, Open WebUI talks to Ollama for answers and to mcpo for tools, and mcpo starts the MCP servers.

The stack on your computer: you open Open WebUI in the browser on port 3000; Open WebUI sends prompts to Ollama and sends OpenAPI calls to mcpo; mcpo talks over stdio to the time, fetch and read-only filesystem MCP servers. Only Open WebUI is reachable from outside the Docker network.

Not sure whether you need mcpo at all? Follow the arrows:

Which way to connect an MCP tool: if the server speaks Streamable HTTP, add it in Open WebUI as an MCP connection; if not, run mcpo and add it as an OpenAPI connection. In both cases turn on native function calling for the model.

One thing trips people up: who makes the request. An admin ("global") tool connection is called by the Open WebUI server, so http://mcpo:8000/time works on the Docker network. A personal ("direct") connection is called by your browser, where mcpo means nothing and localhost means your own machine.

Before you start#

You needNote
LinuxDocker Engine and Compose v2 (docker compose, not the old docker-compose), and permission to run Docker
WindowsDocker Desktop with Linux containers and the WSL2 backend. Open WebUI's docs recommend running Docker commands inside the WSL terminal; the PowerShell commands below are the Docker Desktop version
MacDocker Desktop. Open WebUI supports ARM64 and x86_64. Check that the mcpo image supports your chip (VERIFY)
A tool-capable modelPick an Ollama model that supports tool calling. A weak model may ignore tools even when everything is connected right
Python 3.11+ and uvOnly if you run mcpo on the host instead of in Docker (the mcpo README says 3.8, but its code requires 3.11)

Check your tools:

Run in terminalbash
docker --version
docker compose version

Checklist:

  • [ ] Docker runs (docker ps works)
  • [ ] Port 3000 is free
  • [ ] You know which folder the model may read (we use an empty shared folder)
  • [ ] You backed up any existing Open WebUI data folder

Tested on 2026-10-26: Linux = not run, Windows = not run, Mac = not run. Every command here is checked against the official docs and the open-source files named in Sources; we did not execute them. The official docs are the final word if a command differs.

Step 1: Create the folders and secrets#

All data lives under docker_volumes/ in your home folder. The .env file holds two random secrets: WEBUI_SECRET_KEY (keep it forever - if it changes, saved tool logins break) and MCPO_API_KEY (the password for mcpo).

Linux#

Run in terminalbash
mkdir -p "$HOME/docker_volumes/open-webui/data" "$HOME/docker_volumes/ollama" \
         "$HOME/docker_volumes/mcpo/shared" "$HOME/docker_volumes/mcp-stack/backups"
cd "$HOME/docker_volumes/mcp-stack"
umask 077
printf 'DATA_ROOT=%s/docker_volumes\n' "$HOME" > .env
printf 'WEBUI_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
printf 'MCPO_API_KEY=%s\n' "$(openssl rand -hex 32)" >> .env

Windows (PowerShell)#

Codepowershell
$Root = Join-Path $HOME "docker_volumes"
$Stack = Join-Path $Root "mcp-stack"
"open-webui/data","ollama","mcpo/shared","mcp-stack/backups" | ForEach-Object {
  New-Item -ItemType Directory -Force -Path (Join-Path $Root $_) | Out-Null }
Set-Location $Stack
function New-HexSecret { $b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Fill($b); -join ($b | ForEach-Object { $_.ToString("x2") }) }
@("DATA_ROOT=$($Root.Replace('\','/'))", "WEBUI_SECRET_KEY=$(New-HexSecret)", "MCPO_API_KEY=$(New-HexSecret)") |
  Set-Content -Encoding ascii .env

Do not mix Windows paths and WSL paths in one setup. If you work inside WSL, use the Linux commands.

Mac (Terminal)#

Same commands as Linux. In Docker Desktop, make sure your home folder is allowed under file sharing.

Check it worked#

Run in terminalbash
ls -la ~/docker_volumes

You should see open-webui, ollama, mcpo and mcp-stack. On Windows run dir $Root.

Step 2: Pin the mcpo and Ollama images#

Open WebUI has a real version tag (v0.12.4). mcpo and Ollama are pulled as rolling images, so we pin the exact download by its digest and write it into .env. Rolling tags can change under you.

Linux#

Run in terminalbash
docker pull ghcr.io/open-webui/mcpo:main
docker pull ollama/ollama:latest
printf 'MCPO_IMAGE=%s\n' "$(docker image inspect ghcr.io/open-webui/mcpo:main --format '{{index .RepoDigests 0}}')" >> .env
printf 'OLLAMA_IMAGE=%s\n' "$(docker image inspect ollama/ollama:latest --format '{{index .RepoDigests 0}}')" >> .env
set -a; . ./.env; set +a
docker run --rm --entrypoint python "$MCPO_IMAGE" -c 'import importlib.metadata as m; print("mcpo",m.version("mcpo")); print("mcp",m.version("mcp"))'

Windows (PowerShell)#

Run in terminalpowershell
docker pull ghcr.io/open-webui/mcpo:main
docker pull ollama/ollama:latest
$McpoImage = docker image inspect ghcr.io/open-webui/mcpo:main --format '{{index .RepoDigests 0}}'
$OllamaImage = docker image inspect ollama/ollama:latest --format '{{index .RepoDigests 0}}'
"MCPO_IMAGE=$McpoImage" | Add-Content -Encoding ascii .env
"OLLAMA_IMAGE=$OllamaImage" | Add-Content -Encoding ascii .env
docker run --rm --entrypoint python $McpoImage -c 'import importlib.metadata as m; print("mcpo",m.version("mcpo")); print("mcp",m.version("mcp"))'

Mac (Terminal)#

Same commands as Linux.

Check it worked#

The last command prints two lines like mcpo <version> and mcp <version>. If mcp starts with 2, you hit issue #303. Build a tiny fixed image (the official image keeps its Python in /app/.venv):

Codetext
FROM <paste your MCPO_IMAGE value here>
RUN uv pip install --python /app/.venv/bin/python "mcp<2"

Save it as Dockerfile in a new folder, run docker build -t mcpo-fixed ., and set MCPO_IMAGE=mcpo-fixed in .env. This workaround is our reading of the issue thread and of the official Dockerfile (VERIFY it on your machine).

Step 3: Write config.json for three servers#

mcpo reads one file and gives every server its own address: /time, /fetch, /filesystem. Save this as ~/docker_volumes/mcpo/config.json (Windows: $HOME\docker_volumes\mcpo\config.json). JSON has no comments, so paste it as is.

File contentjson
{
  "mcpServers": {
    "time": {
      "command": "uvx",
      "args": ["--with", "mcp<2", "mcp-server-time", "--local-timezone=Europe/London"]
    },
    "fetch": {
      "command": "uvx",
      "args": ["--with", "mcp<2", "mcp-server-fetch"]
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/shared"]
    }
  }
}

Change the time zone to yours. Notes:

  • /shared is a path inside the container. Step 4 mounts your shared folder there.
  • The mcp<2 bit keeps the Python tool servers on the working 1.x SDK. It must be set on each server separately. The newest mcp-server-time and mcp-server-fetch (2026.10.19 and later) require mcp 2.3, so with this constraint uv quietly picks the previous release (we resolved 2026.10.10 with mcp 1.30.0). If you pin exact versions later, pin that one or a tested newer one.
  • The package names are not version pins. Once it works, pin exact versions (package==X for Python, package@X for npm) that you tested.
  • The official mcpo image contains uvx, npx and Node 22 (from its Dockerfile), so no host install is needed.

Linux#

Save the file where shown above, for example with nano ~/docker_volumes/mcpo/config.json.

Windows (PowerShell)#

Save it with notepad $HOME\docker_volumes\mcpo\config.json. Keep the forward slash in /shared: that path is inside the Linux container.

Mac (Terminal)#

Same as Linux. open -e ~/docker_volumes/mcpo/config.json opens it in TextEdit; use plain text mode.

Check it worked#

Run in terminalbash
python3 -c "import json,os;json.load(open(os.path.expanduser('~/docker_volumes/mcpo/config.json')));print('valid JSON')"

On Windows use Get-Content $Root/mcpo/config.json | ConvertFrom-Json | Out-Null - no error means it is valid JSON.

Step 4: Start the stack with Docker Compose#

Save this as docker-compose.yml in the mcp-stack folder. It has no obsolete version: line and publishes only Open WebUI, only to your own computer.

File contentyaml
services:
  ollama:
    image: ${OLLAMA_IMAGE:?set OLLAMA_IMAGE in .env}
    restart: unless-stopped
    volumes:
      - "${DATA_ROOT}/ollama:/root/.ollama"

  open-webui:
    image: ghcr.io/open-webui/open-webui:v0.12.4
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:8080"
    environment:
      WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:?set WEBUI_SECRET_KEY in .env}
      OLLAMA_BASE_URL: http://ollama:11434
      ENABLE_TOOL_SERVERS: "True"
    volumes:
      - "${DATA_ROOT}/open-webui/data:/app/backend/data"
    depends_on:
      - ollama

  mcpo:
    image: ${MCPO_IMAGE:?set MCPO_IMAGE in .env}
    restart: unless-stopped
    command:
      - "--port"
      - "8000"
      - "--api-key"
      - "${MCPO_API_KEY:?set MCPO_API_KEY in .env}"
      - "--config"
      - "/config/config.json"
    volumes:
      - "${DATA_ROOT}/mcpo/config.json:/config/config.json:ro"
      - "${DATA_ROOT}/mcpo/shared:/shared:ro"

The shared mount is read-only on purpose. The filesystem server advertises write tools, and Docker will refuse them. Open a writable folder only when you really want the model to edit files.

Linux#

Run in terminalbash
docker compose config --quiet
docker compose up -d
docker compose ps

Windows (PowerShell)#

Run in terminalpowershell
docker compose config --quiet
docker compose up -d
docker compose ps

Mac (Terminal)#

Same commands as Linux.

Check it worked#

docker compose ps should list three services as running. Then look at the mcpo log:

Run in terminalbash
docker compose logs --tail=50 mcpo

Expected: mcpo starts and lists the servers from your config. Do not paste a full docker compose config output anywhere - it shows your secrets.

Prefer one container? The Open WebUI project also ships ghcr.io/open-webui/open-webui:v0.12.4-ollama, which has Ollama inside. Run it with docker run -d --name open-webui --restart unless-stopped -p 127.0.0.1:3000:8080 -e WEBUI_SECRET_KEY=<YOUR_KEY> -v "$HOME/docker_volumes/open-webui/data:/app/backend/data" -v "$HOME/docker_volumes/ollama:/root/.ollama" ghcr.io/open-webui/open-webui:v0.12.4-ollama and run mcpo separately. Use it instead of the Compose ollama and open-webui services, never next to them.

mcpo on the host with uvx (optional)#

If you would rather not use Docker for mcpo, this works in Bash, PowerShell and Mac Terminal once uv is installed. Put the constraint on mcpo and on every child server:

Run in terminalbash
uvx --python 3.11 --with "mcp<2" mcpo==0.0.20 --port 8000 --api-key "<YOUR_MCPO_KEY>" --config "/absolute/path/config.json"

On Windows, write the config path with forward slashes or double backslashes. We did not find a documented "listen on localhost only" flag (VERIFY with --help), so on a host install use your firewall to keep port 8000 private.

Step 5: First run in Open WebUI#

  1. Open http://localhost:3000 and create the first account. That account is the admin.
  2. Go to Settings, Admin, Connections and check that Ollama points to http://ollama:11434.
  3. Pull a tool-capable model: docker compose exec ollama ollama pull <your-model>. Chat once without tools to make sure the basics work.
  4. Go to Settings, Admin, Integrations, External Tool Servers and add an OpenAPI connection (not MCP) for each mcpo route:
    • URL: http://mcpo:8000/time
    • If the form asks for the spec: http://mcpo:8000/time/openapi.json
    • Authentication: Bearer, with your MCPO_API_KEY from .env
    • Repeat for /fetch and /filesystem.
  5. Open the model's Advanced Parameters and set Function Calling to Native. It is the default today, and the old Legacy mode is not supported for new setups.
  6. In a new chat, switch the tool on from the tools menu and test one tool at a time:
    • "Use the time tool to tell me the current time."
    • "List the files in the shared folder. Do not change anything."
    • "Fetch this page and summarise it: <a public URL>"

Exact button labels can shift between releases (VERIFY on your version). If the model answers without a tool call, that is not proof the network is broken - check whether a tool call really happened.

Linux#

Open http://localhost:3000 in your browser. The steps above are the same.

Windows (PowerShell)#

Same steps. Run the docker compose commands from the mcp-stack folder (Set-Location $Stack).

Mac (Terminal)#

Same steps as Linux.

A server that speaks Streamable HTTP? Admin, Integrations, External Tool Servers, add a connection of type MCP (Streamable HTTP), enter its MCP endpoint and the auth it needs. Native MCP is admin-only, and with OAuth each user must enable the tool in a chat and approve it once.

Global or personal? Admin connections run from the Open WebUI server and can be limited to users or groups. Personal ("direct") connections run from the browser, need the Direct Tool Servers permission, and cannot see mcpo. In v0.12.4, turning Direct Integrations off also stops saved personal connections from being used.

Put it behind HTTPS with Traefik#

This is only for a server with its own name where you already run Traefik. Home-only users can skip it. The labels below are an example, not an official Open WebUI recipe (VERIFY against your Traefik setup). They assume an entry point called websecure, a certificate resolver called internal, and an external network called proxy (docker network create proxy if missing).

Add this to the open-webui service and delete its ports: entry:

File contentyaml
    environment:
      WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
      OLLAMA_BASE_URL: http://ollama:11434
      ENABLE_TOOL_SERVERS: "True"
      WEBUI_URL: https://<your-domain>
      CORS_ALLOW_ORIGIN: https://<your-domain>
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.openwebui.rule=Host(`<your-domain>`)"
      - "traefik.http.routers.openwebui.entrypoints=websecure"
      - "traefik.http.routers.openwebui.tls=true"
      - "traefik.http.routers.openwebui.tls.certresolver=internal"
      - "traefik.http.services.openwebui.loadbalancer.server.port=8080"
    networks:
      - default
      - proxy

Then add the network at the top level of the file:

File contentyaml
networks:
  proxy:
    external: true

Never add Traefik labels to mcpo or Ollama. WebSockets must pass through the proxy and responses must not be buffered, or chats stall.

Security and testing#

MCP servers run code on your machine, and a web-reachable proxy that can start servers is a way into your system. The official MCP security guidance says so plainly. A key and HTTPS help, but they do not replace limits on what the tools can touch.

  • Keep mcpo and Ollama off the internet. In this stack they publish no ports.
  • Keep Open WebUI sign-in on. Bind it to 127.0.0.1 unless you add Traefik.
  • Mount one dedicated folder, read-only first. Never mount /, your whole home folder, SSH keys, cloud credentials or the Docker socket.
  • Keep .env private: chmod 600 .env on Linux and Mac; on Windows check that only your account can read it. The mcpo key is passed as a command argument, so anyone with Docker access can read it. Do not give untrusted people Docker access.
  • Give each tool only to the users or groups who need it.
  • Treat fetched web pages and tool output as untrusted: a page can contain text that tries to steer the model.
  • Check what is listening:
Run in terminalbash
docker compose ps
curl -I http://127.0.0.1:3000

The curl should answer with HTTP 200 and nothing should answer on port 8000 or 11434 from outside the containers. With Traefik, also run curl -I https://<your-domain>.

Troubleshooting#

ProblemSolution
mcpo fails at start with an import error about streamablehttp_clientMCP SDK 2.x. Use --with "mcp<2" on the host, or the fixed image from Step 2. See issue #303
ClosedResourceErrorThe tool server dropped its session. mcpo 0.0.20 reconnects; check the log for why it keeps dropping
Error decrypting tokens after re-creating the containerWEBUI_SECRET_KEY changed. Put the old key back, or re-authorise the OAuth tools
Failed to connect to MCP server '<id>'Read the log line after it: wrong URL, wrong auth type or slow start. Do not pick Bearer with an empty key
Cannot convert undefined or null to object at Object.entriesAn MCP config was entered as an OpenAPI connection. Use type MCP for native servers and the /time/openapi.json style URL for mcpo
Tool works in your browser but not in a chatNetwork namespace mismatch. Global connections use Docker names (http://mcpo:8000/time); direct connections use browser addresses
Personal tools stopped after upgrading to v0.12.4Direct Integrations is off. Turn it on only if you want personal connections
Slow start of an MCP serverDefault initialize limit is 10 seconds. Set MCP_INITIALIZE_TIMEOUT to 30 on open-webui once the server is healthy
Model never calls the toolUse a tool-capable model and Native function calling, and enable the tool in the chat

Open WebUI backend required#

The exact message is "Open WebUI backend required. Oops! You're using an unsupported method (frontend only)." It means /api/config did not reach the backend. Check that your proxy sends traffic to container port 8080, clear the browser cache and old service workers. It does not mean the image is missing its backend.

Known bugs#

  • mcpo #303: fresh installs resolve MCP 2.x and fail. Open at the time of writing. Workaround above.
  • mcpo #302: after an upstream HTTP error one CPU core can stay at 100 percent. We could not confirm a fix (VERIFY). Watch docker stats and restart mcpo if it happens.

Useful diagnostics:

Run in terminalbash
docker compose ps
docker compose logs --tail=100 mcpo
docker compose logs --tail=100 open-webui
docker stats --no-stream

Updates and backups#

Database changes in a new Open WebUI release can be one-way, so an old image may not read a migrated database. Always keep a backup from before the upgrade.

To update: read the release notes, back up, change the tag in docker-compose.yml, then docker compose pull && docker compose up -d. To update mcpo, pull the image again, check the versions as in Step 2 and update MCPO_IMAGE.

Linux#

Run in terminalbash
cd "$HOME/docker_volumes/mcp-stack"
STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP="$PWD/backups/mcp-stack-$STAMP.tar.gz"
docker compose stop
tar -czf "$BACKUP" -C "$HOME/docker_volumes" open-webui mcpo mcp-stack/docker-compose.yml mcp-stack/.env
docker compose start
tar -tzf "$BACKUP" > /dev/null && echo "backup OK"

The archive holds your secrets: keep a copy off the machine and encrypt it. Add ollama to the tar line only if you want the models in the backup (they are large). To run it every night at 03:00, put the commands in backup.sh and add 0 3 * * * /bin/bash $HOME/docker_volumes/mcp-stack/backup.sh with crontab -e.

Windows (PowerShell)#

Codepowershell
Set-Location $Stack
$Stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$Backup = Join-Path $Stack "backups/mcp-stack-$Stamp.tar.gz"
docker compose stop
tar -czf $Backup -C $Root open-webui mcpo mcp-stack/docker-compose.yml mcp-stack/.env
docker compose start
tar -tzf $Backup | Out-Null; if ($LASTEXITCODE -eq 0) { "backup OK" }

Windows 11 ships tar. For a nightly run, save the commands as backup.ps1 and create a Task Scheduler task at 03:00 that runs pwsh -File <path>\backup.ps1.

Mac (Terminal)#

Same commands as Linux. For the schedule, use crontab -e as above (your Mac must be awake) or a launchd job.

Restore test#

Never test on live data. Unpack the backup somewhere else and start a second copy on another port:

Run in terminalbash
RESTORE="$HOME/docker_volumes/restore-test"
mkdir -p "$RESTORE" && tar -xzf "$BACKUP" -C "$RESTORE"
docker run -d --name open-webui-restore-test -p 127.0.0.1:3001:8080 \
  -e WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" \
  -v "$RESTORE/open-webui/data:/app/backend/data" \
  ghcr.io/open-webui/open-webui:v0.12.4

On Windows use the same docker run with --mount "type=bind,source=$Restore/open-webui/data,target=/app/backend/data". Open http://localhost:3001 and check: your login works, old chats are there, settings and tool connections exist, and the data survives docker restart open-webui-restore-test. This small copy has no Ollama or mcpo behind it; to test real tool calls restore a whole second Compose project with another DATA_ROOT and port. Finish with docker rm -f open-webui-restore-test.

Performance and resources#

  • The model dominates everything: weights, context length and your CPU or GPU. Open WebUI and mcpo are small next to it.
  • The official docs list about 300 MiB idle RAM for Open WebUI (slim image, v0.12.3 test). That is not a minimum for the stack. Measure yours with docker stats while you chat.
  • Every tool you switch on adds its description to the prompt. Enable only what the current chat needs.
  • The first start of a uvx or npx tool downloads packages, so the first call is slow. It needs internet once.
  • Keep one Open WebUI worker for a single-user setup. Do not add workers or replicas without the official scaling guide.
  • Docker Desktop adds its own VM memory. Give it enough in its settings.
  • If you want GPU, it has to reach the container that runs the model (Ollama), not Open WebUI.

FAQ#

Does my local model need to support MCP? No. Open WebUI is the MCP client and runs the tools. The model only needs to be good at tool calling.

Do I still need mcpo now that Open WebUI has native MCP? Only for command-line (stdio) servers and old SSE servers. A server that speaks Streamable HTTP can connect directly.

Why does http://localhost:8000 not work? localhost means whoever makes the request. For an admin connection that is the Open WebUI container, so use http://mcpo:8000/time. For a personal connection it is your browser's computer.

Can the filesystem tool see my whole computer? Only what you mount. Mount one folder, read-only first, and test with files you do not care about.

Can this run offline? Yes once images, models and tool packages are downloaded. A fetch tool still needs the internet, and the first uvx and npx runs download packages.

Sources#

Read on 2026-10-26. Checked against the official docs on 2026-10-26; not run on a test machine.