Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
.github
.venv
env
data
internal
demo
tests
.pytest_cache
__pycache__
**/__pycache__
*.pyc
*.pyo
*.pyd
*.log
.DS_Store
59 changes: 59 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Docker Publish

on:
push:
branches: [main]
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/razpines/unityrag
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag

- name: Build and push image
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Build image only (manual/non-publish refs)
if: github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/v')
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: false
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
UNITY_DOCS_MCP_ROOT=/app \
UNITY_DOCS_MCP_CONFIG=/app/config.docker.yaml \
UNITY_DOCS_MCP_HOST=0.0.0.0 \
UNITY_DOCS_MCP_PORT=8765

WORKDIR /app

COPY pyproject.toml README.md LICENSE ./
COPY config.yaml config.docker.yaml ./
COPY src ./src

RUN python -m pip install --upgrade pip && \
python -m pip install .

EXPOSE 8765

VOLUME ["/app/data"]

CMD ["unitydocs-mcp-http"]
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
![UnityRAG demo comparison](demo/assets/ad_demo_readme_dark.png)

[![CI](https://github.com/Razpines/UnityRAG/actions/workflows/ci.yml/badge.svg)](https://github.com/Razpines/UnityRAG/actions/workflows/ci.yml)
[![Docker GHCR](https://img.shields.io/badge/Docker-GHCR-2496ED?logo=docker&logoColor=white)](https://github.com/Razpines/UnityRAG/pkgs/container/unityrag)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.12%2B-blue)](pyproject.toml)
[![GitHub Stars](https://img.shields.io/github/stars/Razpines/UnityRAG)](https://github.com/Razpines/UnityRAG/stargazers)
Expand Down Expand Up @@ -45,6 +46,50 @@ unitydocs doctor
unitydocs doctor --json
```

## Docker Quick Start (CPU-only)

Use the official CPU-first container image to run the MCP HTTP server without local Python/venv setup.

Run:

```bash
docker run -d --name unityrag \
-p 8765:8765 \
-v unityrag-data:/app/data \
ghcr.io/razpines/unityrag:latest
```

Wait / first startup:
- First startup is slow: the container downloads Unity docs, bakes artifacts, and builds the FTS index.
- Watch progress with `docker logs -f unityrag`.
- Data persists across container recreation via the mounted volume (`/app/data`).

Verify:
- MCP HTTP endpoint is `http://localhost:8765/mcp`.
- A plain `GET /mcp` may return `406 Not Acceptable`; this is expected for streamable HTTP when the client does not send `Accept: text/event-stream`.

Connect (Codex CLI):

```bash
codex mcp add unity-docs-http --url http://localhost:8765/mcp
```

Common adjustments:
- Port `8765` already in use: map a different host port, for example `-p 8876:8765`, then connect to `http://localhost:8876/mcp`.
- Remote-hosted server: use `http://<host>:8765/mcp` (or your remapped host port).
- The Docker container must stay running while your MCP client is connected.

Container defaults (advanced):
- `UNITY_DOCS_MCP_ROOT=/app`
- `UNITY_DOCS_MCP_CONFIG=/app/config.docker.yaml` (FTS-only / CPU-safe)
- `UNITY_DOCS_MCP_HOST=0.0.0.0`
- `UNITY_DOCS_MCP_PORT=8765`

Client config details (Docker-hosted MCP):
- `docs/docker_http_clients.md`

Initial Docker scope is CPU-only / FTS-only. GPU images and Docker Compose quick start are tracked separately.

## Why UnityRAG

- Uses official Unity offline docs for versioned retrieval.
Expand Down Expand Up @@ -95,6 +140,7 @@ pytest
## Docs

- Developer details: `docs/README.md`
- Docker-hosted MCP clients: `docs/docker_http_clients.md`

## Content and Licensing

Expand Down
3 changes: 3 additions & 0 deletions config.docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CPU-only container overrides layered on top of config.yaml.
index:
vector: "none"
74 changes: 74 additions & 0 deletions docs/docker_http_clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Docker-Hosted MCP Clients (HTTP)

This page shows how to connect MCP clients to a UnityRAG server running in Docker.

The Docker image runs a streamable HTTP MCP server at:
- `http://localhost:8765/mcp` (same machine)
- `http://<host>:8765/mcp` (remote host)

## Before You Connect

- Start the container (see `README.md` Docker quick start).
- First startup can take a while (download + bake + index).
- Watch progress with `docker logs -f unityrag`.
- A plain `GET /mcp` may return `406 Not Acceptable`; that is expected for streamable HTTP when the request does not accept `text/event-stream`.

## Codex (Verified)

Add the Docker-hosted MCP server:

```bash
codex mcp add unity-docs-http --url http://localhost:8765/mcp
```

Remote host example:

```bash
codex mcp add unity-docs-http --url http://<host>:8765/mcp
```

Confirm:

```bash
codex mcp list
```

## Claude Desktop (Planned: verify exact HTTP config schema)

We will document an exact Claude Desktop HTTP MCP config snippet after local verification.

Expected shape (conceptually):
- Use an HTTP/streamable MCP server entry (not `command`/`args`)
- Point it to `http://localhost:8765/mcp` (or remote host URL)
- No UnityRAG process path is needed because Docker is running the server

## Cline / GitHub Copilot / Other MCP Clients (Compatibility Planning)

Use the same endpoint concept:
- `http://localhost:8765/mcp` (local)
- `http://<host>:8765/mcp` (remote)

Client-specific config format varies by product and version. We only publish exact snippets after end-to-end verification.

## Compatibility Status (Docker-hosted HTTP MCP)

| Client | How to Configure | Status | Notes |
| --- | --- | --- | --- |
| Codex | CLI (`codex mcp add ... --url ...`) | Verified | Uses streamable HTTP MCP URL directly |
| Claude Desktop | Desktop config (HTTP MCP entry) | Planned | Exact schema/version to verify before publishing snippet |
| Cline | Client config (HTTP MCP entry) | Planned | Format varies; verify before publishing |
| GitHub Copilot | Product-specific MCP integration | Planned | Verify product support/path before publishing |

## Troubleshooting

- Port collision:
- Start Docker with a different host port, e.g. `-p 8876:8765`
- Connect clients to `http://localhost:8876/mcp`
- Container not running:
- `docker ps`
- `docker logs -f unityrag`
- Remote host unreachable:
- Check firewall / LAN routing / port exposure
- Confirm the server is listening on `0.0.0.0` (Docker image default)
- Client gets an HTTP error on manual browser/curl check:
- `406` on plain `GET` is usually expected for this transport
3 changes: 3 additions & 0 deletions src/unity_docs_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ def deep_update(target: Dict[str, Any], incoming: Dict[str, Any]) -> Dict[str, A


def _repo_root() -> Path:
root_override = os.environ.get("UNITY_DOCS_MCP_ROOT")
if root_override:
return Path(root_override).expanduser()
return Path(__file__).resolve().parents[2]


Expand Down
19 changes: 19 additions & 0 deletions tests/test_config_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,22 @@ def test_load_config_explicit_path_has_highest_precedence(monkeypatch, tmp_path:

cfg = config_mod.load_config(explicit_cfg)
assert cfg.unity_version == "6000.0"


def test_load_config_uses_unity_docs_mcp_root_for_base_layers(monkeypatch, tmp_path: Path):
_write(
tmp_path / "config.yaml",
"""
paths:
root: "data/unity/from-root-override"
index:
vector: "none"
""".strip()
+ "\n",
)
monkeypatch.setenv("UNITY_DOCS_MCP_ROOT", str(tmp_path))
monkeypatch.delenv("UNITY_DOCS_MCP_CONFIG", raising=False)

cfg = config_mod.load_config()
assert cfg.paths.root == "data/unity/from-root-override"
assert cfg.index.vector == "none"