# ChromaDB Remote MCP Server [![MCP](https://img.shields.io/badge/MCP-Streamable%20HTTP-blue)](https://modelcontextprotocol.io) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)](https://www.typescriptlang.org/) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![MseeP.ai](https://img.shields.io/badge/MseeP.ai-Audited-4c1)](https://mseep.ai/app/meloncafe-chromadb-remote-mcp) [![codecov](https://codecov.io/gh/meloncafe/chromadb-remote-mcp/graph/badge.svg?token=0abUQsve4y)](https://codecov.io/gh/meloncafe/chromadb-remote-mcp) [![DeepSource](https://app.deepsource.com/gh/meloncafe/chromadb-remote-mcp.svg/?label=Active+Issues&show_trend=true&token=Mzfb6tMnlzBIxaJO9CsYO3e8)](https://app.deepsource.com/gh/meloncafe/chromadb-remote-mcp/) A **Streamable HTTP** MCP (Model Context Protocol) server that provides remote access to ChromaDB for AI assistants like Claude. Enables semantic search and vector database operations from mobile devices and remote locations. > **Note**: This project uses MCP Streamable HTTP (2025-03-26 spec). SSE transport is deprecated. [한국어 문서](README.ko.md) --- ## Cross-Platform AI Memory Server **Compatible with ALL major AI platforms:** - Claude (Desktop, Mobile, Code) - Gemini (CLI, Code Assist) - Cursor, Cline, Windsurf, VS Code Copilot - and use Remote MCP with any other MCP-compatible client ## Features Remote MCP server that enables all Claude clients (Desktop, Code, Mobile) to access the same self-hosted ChromaDB instance. - **Shared Memory Across Devices** - All Claude clients use the same ChromaDB instance - **Self-Hosted & Private** - Your data stays on your infrastructure - **Remote Access** - Connect from anywhere via Tailscale or public internet - **Complete ChromaDB Support** - All CRUD operations via MCP tools - **REST API Proxy** - Direct ChromaDB access for Python/JavaScript - **Unified Authentication** - Single token protects both MCP and REST API endpoints - **Easy Deployment** - One-command installation with Docker --- ## Architecture ### Overview ``` ┌──────────────────────────────┐ ┌──────────────┐ │ Claude Desktop + Mobile │ │ Claude Code │ │ (Custom Connector - synced) │ │ (CLI setup) │ └──────────────┬───────────────┘ └──────┬───────┘ │ │ │ MCP Remote Connector │ └─────────────┬───────────────┘ │ HTTPS ┌─────────▼──────────┐ │ Remote MCP │ │ Server (Node.js) │ │ │ │ • Auth Gateway │ │ • MCP Protocol │ │ • REST API Proxy │ └─────────┬──────────┘ │ ┌─────────▼──────────┐ │ ChromaDB │ │ (Vector Database) │ │ │ │ • Embeddings │ │ • Collections │ │ • Semantic Search │ └────────────────────┘ ``` **How Clients Connect:** - **Claude Desktop + Mobile**: Set up once using custom connector in Claude Desktop, and it automatically syncs to the mobile app. Both share the same connection automatically. - **Claude Code**: Requires separate setup using `claude mcp add` CLI command. All clients access the same self-hosted ChromaDB through this remote MCP server. Vector embeddings and semantic search results persist across all platforms. ### API Endpoints | Path | Purpose | Client | Authentication | | --------------- | ----------------- | -------------------------- | -------------- | | `/mcp` | MCP Protocol | Claude Desktop/Code/Mobile | ✅ | | `/api/v2/*` | ChromaDB REST API | Python | ✅ | | `/docs` | Swagger UI | Browser (API docs) | ✅ | | `/openapi.json` | OpenAPI Spec | API tools | ✅ | | `/health` | Health check | Monitoring | ❌ | ### How It Works 1. **Claude Desktop/Mobile**: Add MCP server via custom connector (syncs automatically between devices) 2. **Claude Code**: Add MCP server using `claude mcp add` CLI command 3. **Remote MCP Server** authenticates requests and translates MCP protocol to ChromaDB operations 4. **ChromaDB** stores and retrieves vector embeddings for semantic search 5. **Python** can also access ChromaDB directly via the proxied REST API **Benefits:** - Same vector database across all clients - Desktop and mobile share connection automatically - Self-hosted and private - Persistent memory across app restarts - Single source of truth for embeddings --- ## Quick Start ### One-Command Installation ```bash curl -fsSL https://raw.githubusercontent.com/meloncafe/chromadb-remote-mcp/release/scripts/install.sh | bash ``` This will: 1. Download `docker-compose.yml` and `.env.example` 2. Auto-detect Docker Compose command (`docker-compose` or `docker compose`) 3. Auto-generate a secure authentication token (optional) 4. Configure ChromaDB data storage location (Docker volume, local directory, or custom path) 5. Pull Docker images 6. Display your authentication token and connection URL ### Manual Installation #### Option 1: Docker (Recommended - Pre-built Image) ```bash # Download configuration files mkdir chromadb-remote-mcp && cd chromadb-remote-mcp curl -O https://raw.githubusercontent.com/meloncafe/chromadb-remote-mcp/release/docker-compose.yml curl -O https://raw.githubusercontent.com/meloncafe/chromadb-remote-mcp/release/.env.example # Configure environment cp .env.example .env # Edit .env and set: # - MCP_AUTH_TOKEN (see token generation below) # - PORT (default: 8080) # - CHROMA_DATA_PATH (default: chroma-data) # Start services docker compose up -d # or: docker-compose up -d (for older versions) # Check health curl http://localhost:8080/health # View logs docker compose logs -f ``` #### Option 2: Build from Source ```bash # Clone repository git clone https://github.com/meloncafe/chromadb-remote-mcp.git cd chromadb-remote-mcp # Configure environment cp .env.example .env # Edit .env with your configuration # Start with docker-compose (builds image from source) docker compose -f docker-compose.dev.yml up -d # or: docker-compose -f docker-compose.dev.yml up -d (for older versions) ``` #### Option 3: Local Development ```bash # Clone and install git clone https://github.com/meloncafe/chromadb-remote-mcp.git cd chromadb-remote-mcp yarn install # Configure environment cp .env.example .env # Edit .env file # Build and run yarn build yarn start ``` ### Generate Secure Token For production use, generate a secure token for `MCP_AUTH_TOKEN` in `.env`: ```bash # Method 1: Node.js (Recommended) node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" # Method 2: OpenSSL openssl rand -base64 32 | tr '+/' '-_' | tr -d '=' ``` Copy the generated token and paste it into your `.env` file: ```env MCP_AUTH_TOKEN=your-generated-token-here ``` ### Server Endpoints - MCP: `http://localhost:8080/mcp` (via Caddy proxy) - Health: `http://localhost:8080/health` - ChromaDB API: `http://localhost:8080/api/v2/*` - Swagger UI: `http://localhost:8080/docs` --- ## Configuration ### Environment Variables (.env file) All configuration is done through the `.env` file. Copy `.env.example` to `.env` and customize: ```bash cp .env.example .env ``` | Variable | Description | Default | Required | | ------------------- | -------------------------------------------------------------------- | ------------------ | --------------------------- | | `PORT` | External port (Caddy reverse proxy) | `8080` | No | | `CHROMA_DATA_PATH` | ChromaDB data storage path (volume name, `./data`, or absolute path) | `chroma-data` | No | | `CHROMA_HOST` | ChromaDB host (internal) | `chromadb` | No | | `CHROMA_PORT` | ChromaDB port (internal) | `8000` | No | | `CHROMA_TENANT` | ChromaDB tenant | `default_tenant` | No | | `CHROMA_DATABASE` | ChromaDB database | `default_database` | No | | `MCP_AUTH_TOKEN` | Authentication token for MCP and REST API | - | **Yes** (for public access) | | `CHROMA_AUTH_TOKEN` | ChromaDB auth token (if ChromaDB requires auth) | - | No | | `RATE_LIMIT_MAX` | Max requests per IP per 15 minutes | `100` | No | | `ALLOWED_ORIGINS` | Comma-separated list of allowed origins (DNS rebinding protection) | - | No | | `CHROMA_SERVER_VERSION` | Chroma server version declared by the operator — used by `chroma_get_server_version` and `/health/detail` as the trusted version source. **Warning: with the `chromadb/chroma:latest` image this value does not follow image updates and can drift — pin a version tag and set this to the same value.** | `unknown` when unset; docker-compose.yml default `1.5.9` | No | | `MCP_SERVER_VERSION` | MCP wrapper version — set automatically in release images (release tag → `VERSION` build-arg); no need to set it manually | `dev` when unset | No | ### Authentication **IMPORTANT:** For public internet access (Tailscale Funnel, Cloudflare Tunnel, etc.), you **must** set `MCP_AUTH_TOKEN` in your `.env` file. Generate a secure token: ```bash # Method 1: Node.js (Recommended - from .env.example) node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" # Method 2: OpenSSL openssl rand -base64 32 | tr '+/' '-_' | tr -d '=' ``` Edit your `.env` file: ```env MCP_AUTH_TOKEN=your-generated-token-here ``` Then restart the services: ```bash docker compose restart # or: docker-compose restart ``` **Supported authentication methods (v2.0.0):** 1. **`Authorization: Bearer TOKEN`** — only supported way to send `MCP_AUTH_TOKEN`. - Recommended for service-to-service callers (API clients, scripts, MCP relays). - Compliant with MCP specification. - Example: `curl -H "Authorization: Bearer YOUR_TOKEN" https://your-server.com/mcp` 2. **OAuth 2.1 / OpenID Connect** — recommended for human users. - Set `OIDC_ISSUERS` (comma-separated issuer URLs) or `OIDC_PRESET=google,github,microsoft`. - Set `OIDC_AUDIENCE` to the resource identifier (typically your MCP server's public URL). - The server publishes RFC 9728 Protected Resource Metadata at `/.well-known/oauth-protected-resource`. - 401 responses include `WWW-Authenticate: Bearer error="...", resource_metadata="..."` per RFC 6750. > **Removed in v2.0.0:** `X-Chroma-Token` header and `?apiKey=` / `?token=` / `?api_key=` query-parameter auth are no longer accepted. Clients that previously used those paths must migrate to `Authorization: Bearer`. The `ALLOW_QUERY_AUTH` env var is ignored. ### Origin Header Validation (DNS Rebinding Protection) The server validates the `Origin` header for browser requests to prevent DNS rebinding attacks. This security feature is enabled by default and protects your local MCP server from malicious websites. **Default allowed origins (always permitted):** - **Localhost variants**: `localhost`, `127.0.0.1`, `[::1]` - **Claude.ai domains**: `https://claude.ai`, `https://api.anthropic.com` **Configure additional allowed origins:** If you need to allow additional web applications or custom domains, add them to `ALLOWED_ORIGINS` in your `.env` file: ```env # Add additional custom domains (Claude.ai is already allowed by default) ALLOWED_ORIGINS=https://myapp.com,https://yourdomain.com ``` **When to configure ALLOWED_ORIGINS:** - ✅ Using Claude Desktop Custom Connector → **No configuration needed** (allowed by default) - ✅ Accessing from custom web applications → Add your application's domain - ✅ Using Swagger UI remotely → Add your server's domain - ❌ Using Claude Code CLI → Not needed (no Origin header) - ❌ Using Python/JavaScript clients → Not needed (no Origin header) - ❌ Local development only → Not needed (localhost is allowed by default) **Example configurations:** ```env # For custom web application ALLOWED_ORIGINS=https://myapp.com,https://app.mycompany.com # Multiple custom domains (comma-separated, spaces are trimmed) ALLOWED_ORIGINS=https://myapp.com, https://api.example.com, https://dashboard.mycompany.com # Leave empty if you only need Claude.ai and localhost ALLOWED_ORIGINS= ``` **Note:** Claude.ai domains (`https://claude.ai`, `https://api.anthropic.com`) and localhost are always allowed, even if `ALLOWED_ORIGINS` is empty. Server-to-server requests (without Origin header) are always permitted. ### Data Storage Configuration ChromaDB data can be stored in three ways: 1. **Docker volume (default)**: `CHROMA_DATA_PATH=chroma-data` - Managed by Docker - Survives container restarts - Use `docker volume ls` and `docker volume inspect chroma-data` to locate 2. **Local directory**: `CHROMA_DATA_PATH=./data` - Easy to backup and access - Stored in installation directory 3. **Custom path**: `CHROMA_DATA_PATH=/path/to/data` - Must be an absolute path - Useful for mounting external storage After changing `CHROMA_DATA_PATH`, restart the services: ```bash docker compose restart ``` --- ## Connecting Claude ### Claude Desktop + Mobile **Method 1: Custom Connector (Recommended - Pro/Team/Enterprise)** 1. Open Claude Desktop → Settings → Integrations → Custom Connector 2. Click "Add Custom Server" 3. Enter: - **Name**: `ChromaDB` - **URL**: `https://your-server.com/mcp` (set `Authorization: Bearer YOUR_TOKEN` in the connector's header config) > **Note**: Custom connector automatically syncs to the mobile app. Authentication is mandatory for remote access. **Method 2: mcp-remote Wrapper (Free/Pro Users)** If you don't have access to Custom Connectors, use the `mcp-remote` package as a workaround: **Configuration file location:** - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` **Add to configuration file:** ```json { "mcpServers": { "chromadb": { "command": "npx", "args": ["-y", "mcp-remote", "https://your-server.com/mcp", "--header", "Authorization: Bearer YOUR_TOKEN"] } } } ``` Restart Claude Desktop after editing the file. > **Important**: Remote MCP servers cannot be configured directly in `claude_desktop_config.json` using `streamableHttp` transport. You must either use Custom Connectors or the `mcp-remote` wrapper package. ### Claude Code **CLI Command:** ```bash # Without authentication claude mcp add --transport http chromadb https://your-server.com/mcp # With authentication (Query Parameter - Recommended) claude mcp add --transport http chromadb https://your-server.com/mcp \ --header "Authorization: Bearer YOUR_TOKEN" # With authentication (Header) claude mcp add --transport http chromadb https://your-server.com/mcp \ --header "Authorization: Bearer YOUR_TOKEN" # Verify claude mcp list ``` --- ## Available Tools (v2.2.0) The MCP server provides these tools for Claude. v2.2.0 expands coverage to 30 tools across collection / document / search / fork / client-info / admin / destructive groups. ### Collection Management - `chroma_list_collections` - List all collections (with `limit` / `offset`) - `chroma_create_collection` - Create a new collection (`configuration` / `schema` optional) - `chroma_get_or_create_collection` - Idempotent create-or-get (v2.2.0) - `chroma_modify_collection` - Rename / change metadata or configuration (v2.2.0) - `chroma_delete_collection` - Delete a collection - `chroma_get_collection_info` - Get collection metadata - `chroma_get_collection_count` - Get document count (`read_level` optional) - `chroma_count_collections` - Total collection count (v2.2.0) - `chroma_peek_collection` - Preview collection contents ### Document Operations - `chroma_add_documents` - Add documents (with `uris` for multi-modal) - `chroma_upsert_documents` - Idempotent insert-or-update (v2.2.0) - `chroma_query_documents` - Semantic search (with `query_uris` / `ids` pre-filter) - `chroma_get_documents` - Retrieve documents (`read_level` optional) - `chroma_update_documents` - Update existing documents (with `embeddings` / `uris`) - `chroma_delete_documents` - Delete by `ids` and/or `where` / `where_document` filter ### Server Info (v2.2.0) - `chroma_heartbeat` - Server heartbeat (nanosecond timestamp) - `chroma_get_server_version` - Server version string - `chroma_get_max_batch_size` - Max batch size (for client-side splitting) - `chroma_get_user_identity` - Current tenant + databases ### Distributed/Cloud-only — opt-in (`CHROMA_DISTRIBUTED_TOOLS_ENABLED=true`) These 4 tools require ChromaDB's **distributed executor** (the executor is the chromadb-server-internal frontend layer, not an algorithmic distribution requirement). The single-node open-source server (`chromadb/chroma:latest` docker) ships with the **local executor**, which has these methods hard-coded as `unimplemented` in [`rust/frontend/src/executor/local.rs`](https://github.com/chroma-core/chroma/blob/main/rust/frontend/src/executor/local.rs) and [`rust/types/src/api_types.rs`](https://github.com/chroma-core/chroma/blob/main/rust/types/src/api_types.rs). To use them you need either Chroma Cloud (`CloudClient`) or a self-hosted distributed Chroma deployment (Kubernetes multi-component: frontend + query executor + WAL + compactor + object storage). Hidden by default so single-node deployments don't waste LLM context on tools that always return `"not implemented for local executor"` / `"unsupported for local chroma"`. - `chroma_search` - Hybrid dense + sparse search (RRF). The algorithm itself works on a single node; chromadb open-source simply hasn't implemented the `search()` endpoint in the local executor. - `chroma_fork_collection` - Zero-copy fork (segment-level operation on object storage — architecturally requires the distributed compactor/storage stack). - `chroma_get_fork_count` - Fork metadata lookup (depends on the distributed metadata store). - `chroma_get_indexing_status` - WAL offset + compactor index progress (requires the distributed WAL/compactor services). ### Admin — opt-in (`CHROMA_ADMIN_TOOLS_ENABLED=true`) - `chroma_admin_create_database` / `chroma_admin_get_database` / `chroma_admin_list_databases` - `chroma_admin_create_tenant` / `chroma_admin_get_tenant` ### Destructive — opt-in (`CHROMA_ALLOW_DESTRUCTIVE_OPS=true`) Calls emit a `[DESTRUCTIVE]` audit line. - `chroma_reset_database` - Reset entire database (irreversible) - `chroma_admin_delete_database` - Delete a database (requires both flags) --- ## Using ChromaDB from Python The MCP server proxies all ChromaDB REST API endpoints, allowing direct access from Python clients. ### Python Example ```python import chromadb # HTTPS (Tailscale Funnel, public deployment) client = chromadb.HttpClient( host="your-server.com", port=443, ssl=True, headers={ "Authorization": "Bearer YOUR_TOKEN" } ) # Local development (HTTP) client = chromadb.HttpClient( host="localhost", port=8080, ssl=False, headers={ "Authorization": "Bearer YOUR_TOKEN" } ) # Usage collection = client.create_collection("my_collection") collection.add( documents=["Document 1", "Document 2"], ids=["id1", "id2"] ) results = collection.query(query_texts=["query"], n_results=2) ``` Alternative authentication: ```python from chromadb.config import Settings client = chromadb.HttpClient( host="your-server.com", port=443, ssl=True, settings=Settings( chroma_client_auth_provider="chromadb.auth.token_authn.TokenAuthClientProvider", chroma_client_auth_credentials="YOUR_TOKEN" ) ) ``` ### API Documentation Visit `https://your-server.com/docs` for Swagger UI documentation of all ChromaDB REST API endpoints. --- ## Deployment ### Deploying with deploy.sh `deploy.sh` wraps the Docker Compose lifecycle of this repository's `docker-compose.yml` in one script. It is host independent: everything host specific lives in `.env` (see `.env.example`) and optional extra compose files, never in the script. | Command | What it does | |---|---| | `./deploy.sh install` | First-time setup. Creates `.env` from `.env.example` when missing and stops so you can edit it; otherwise pulls images and starts services | | `./deploy.sh update` | Pulls images, recreates containers (`up -d --force-recreate`), prunes dangling images, syncs `CHROMA_SERVER_VERSION` in `.env`, then prints the version information reusing that single judgment | | `./deploy.sh start` | Starts services (`up -d`) | | `./deploy.sh stop` | Stops and removes the containers (`down`); data is kept | | `./deploy.sh restart` | Restarts the containers. Does not re-read `.env` — use `update` or `start` after changing it | | `./deploy.sh status` | `docker compose ps` plus the state of the chromadb / chroma-mcp-server / caddy-proxy containers | | `./deploy.sh logs [service]` | Follows logs (`--tail=100`), e.g. `./deploy.sh logs mcp-server` | | `./deploy.sh version` | Shows the mcp image and running-container versions and the pinned chroma image, resolves the Chroma server version from the running image digest, and records it in `.env` | | `./deploy.sh backup` | Archives the chroma data to `backups/chromadb_data_.tar.gz` (see below) | | `./deploy.sh self-update [tag]` | Replaces `deploy.sh`, `docker-compose.yml`, `docker-compose.traefik.example.yml` and `.env.example` with the ones from a GitHub release (default: latest). `.env`, local compose overrides and data are never touched (see below) | `./deploy.sh help` prints the usage. **Settings** — read from the host's `.env` (a process environment variable of the same name takes precedence): | Key | Meaning | Default | |---|---|---| | `COMPOSE_FILE` | Compose files merged in order, separated by `:` | `docker-compose.yml` | | `COMPOSE_PATH_SEPARATOR` | Separator for the `COMPOSE_FILE` list | `:` | | `COMPOSE_PROJECT_NAME` | Compose project name; it prefixes the network and the named volume (`_chroma-data`). Changing it after the first start gives a named-volume stack a new, empty volume — a bind path keeps its data | deploy directory name | | `CHROMA_DATA_PATH` | Chroma data: the compose named volume `chroma-data`, or an absolute / `./relative` bind path | `chroma-data` (named volume) | | `PORT` | Host port published by caddy | `8080` | | `MCP_IMAGE_TAG` | Tag of the `devsaurus/chromadb-remote-mcp` image | `latest` | | `DEPLOY_LANG` | Message language of `deploy.sh`: `en` or `ko` | from the locale | `DRY_RUN=1`, `DEPLOY_DIR=` (target directory, for tests) and `CHROMA_IMAGE=` (image for the `backup` tar when no chromadb container exists) are read from the process environment. **One stack per host** — `docker-compose.yml` fixes `container_name` (`chromadb`, `chroma-mcp-server`, `caddy-proxy`), so changing only `COMPOSE_PROJECT_NAME` does not let a second stack run on the same host: the container names collide. **Compose file selection** — `COMPOSE_FILE` is a list merged in order (later files override earlier ones), exactly like `docker compose`. Relative entries resolve against the deploy directory, absolute entries are used unchanged. `deploy.sh` passes `--project-directory` on every call, so `.env` and relative paths always refer to the deploy directory. **`DRY_RUN=1`** — prints every docker command prefixed `[DRY-RUN]`, calls no docker and changes no file; it works even without docker installed. `install` prints its plan without copying `.env` or creating `backups/`; `version` / `update` skip the digest judgment, the `.env` write and the mcp-server recreation. A missing `.env` is downgraded to a warning: the plan is printed from the committed `.env.example` / compose defaults and no file is created. Without `DRY_RUN`, a missing `.env` is still an error. ```bash DRY_RUN=1 ./deploy.sh update ``` **Output language** — help and log messages are English or Korean. The language is `DEPLOY_LANG` (environment, then `.env`); if unset, the first non-empty of `LC_ALL`, `LC_MESSAGES`, `LANG`: a value starting with `ko` selects Korean, anything else English. **`CHROMA_SERVER_VERSION` handling** — the Chroma server reports a hardcoded `1.0.0`, so `update` / `version` compare the RepoDigest of the chromadb container's image with the digest pinned in the compose file and then with the Docker Hub digest of the pinned tag, and record the result in `.env`. When the value changed, mcp-server alone is recreated with `up -d`. If the version cannot be determined (including a match on the Docker Hub `latest` tag alone), an existing `.env` value is preserved with a warning; `unknown` is written only when no value exists yet. **Backup** — `./deploy.sh backup` reads the data through a throwaway container with a read-only mount, so root-owned bind directories and named volumes both work. The data location is detected automatically: the existing chromadb container's `/data` mount first, otherwise `CHROMA_DATA_PATH` from `.env` (an absolute, `./` or `../` path is a bind mount; a bare name — the default `chroma-data` — is the compose named volume `_`). The archive is written as `.tar.gz.partial` and renamed on success (removed on failure). chromadb is stopped for a consistent snapshot and started again only if it was running before the backup. If the backup receives `SIGINT` or `SIGTERM` (for example Ctrl+C), it removes the `.tar.gz.partial` file, starts chromadb again when it was running before the backup, and exits with code 130 (`SIGINT`) or 143 (`SIGTERM`). **Self-update** — `./deploy.sh self-update [tag]` downloads the repository-managed files from a GitHub release tag (default: the latest release; `DEPLOY_TAG` or `--tag` also work) into a staging directory, validates them (non-empty, `bash -n` for the script, `services:` for compose files, and `SHA256SUMS` when the release publishes one), backs up the current files to `backups/self-update__/`, replaces each file atomically (temp file + `mv`), and restores the backup on any failure or interrupt. It shows the current and target version (the installed tag is recorded in `.deploy-release`). `.env`, local compose overrides that are not part of the release, and the data directory are never touched. It refuses to run inside a git checkout (use `git pull`) and supports `DRY_RUN=1`. `DEPLOY_REPO` (default `meloncafe/chromadb-remote-mcp`) and `SELF_UPDATE_FILES` override the source repository and the file list. `update` (images) and `self-update` (scripts/compose) are deliberately separate: swapping the compose file in the middle of `update` would change what the pull/recreate runs against, so `update` only prints a hint and `self-update` ends by telling you to run `update`. **Behind a Traefik reverse proxy** — [`docker-compose.traefik.example.yml`](docker-compose.traefik.example.yml) is an optional example override with placeholder variables. Copy it, keep the copy on the server, and select it through the compose file list in `.env`: ```bash cp docker-compose.traefik.example.yml docker-compose.traefik.yml # .env COMPOSE_FILE=docker-compose.yml:docker-compose.traefik.yml TRAEFIK_HOST=mcp.example.com TRAEFIK_CERT_RESOLVER=your-resolver ./deploy.sh update ``` `TRAEFIK_HOST` and `TRAEFIK_CERT_RESOLVER` are required. Optional: `TRAEFIK_NETWORK` (external network Traefik is attached to, which must already exist; default `traefik`), `TRAEFIK_ENTRYPOINT` (default `websecure`), `TRAEFIK_MIDDLEWARES` (comma-separated; see the note below) and `TRAEFIK_ROUTER_NAME` (default `chromadb-mcp`). Traefik router and service names are global, so pick a `TRAEFIK_ROUTER_NAME` that no other container on that Traefik already uses. The example removes the base file's host port mapping with `!reset`, which requires Docker Compose 2.24 or newer. The `middlewares` label in `docker-compose.traefik.example.yml` is commented out by default, so no middleware is attached. Once you uncomment the label, `TRAEFIK_MIDDLEWARES` becomes required: not only an unset variable but also an empty value (`TRAEFIK_MIDDLEWARES=`) aborts with `required variable TRAEFIK_MIDDLEWARES is missing a value`. To run without middlewares, keep the label commented out. ### Option 1: Tailscale VPN (Recommended) **Secure access within your Tailscale network:** ```bash # Start services docker compose up -d # Enable Tailscale Serve (HTTPS with automatic certificates) tailscale serve https / http://127.0.0.1:8080 # Check status tailscale serve status ``` Your server is now accessible at `https://your-machine.tailXXXXX.ts.net` to all devices in your Tailnet. **Advantages:** - Automatic HTTPS certificates - No public internet exposure - Encrypted VPN tunnel - Authentication optional (VPN provides security layer) ### Option 2: Tailscale Funnel (Public Internet) **To use Claude Desktop UI Custom Connector or share publicly:** ```bash # Enable Funnel (allows public internet access) tailscale funnel 8080 on tailscale serve https / http://127.0.0.1:8080 # Verify Funnel is active tailscale serve status # Should show "Funnel on" ``` > **Warning**: This exposes your server to the public internet. **Authentication is mandatory!** Set `MCP_AUTH_TOKEN` in your environment. **Disable Funnel:** ```bash tailscale funnel 8080 off ``` ### Option 3: Cloudflare Tunnel ```bash # Install cloudflared curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared chmod +x cloudflared # Authenticate ./cloudflared tunnel login # Create tunnel ./cloudflared tunnel create chroma-mcp # Run tunnel ./cloudflared tunnel --url http://localhost:3000 ``` ### Option 4: Nginx Reverse Proxy ```nginx server { listen 80; server_name your-domain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` --- ## Security ### Code Quality & Security Analysis This project follows strict security practices and has resolved all security issues identified by static analysis: - ✅ **Zero Active Issues**: All OWASP and CWE security findings have been resolved - 🔒 **Static Analysis**: Continuous monitoring with [DeepSource](https://app.deepsource.com/report/1328a083-a457-4598-b56f-e64dafdbcc28) - 🛡️ **Security Standards**: Compliant with OWASP Top 10 and Node.js security best practices - 📊 **Automated Scanning**: Dependabot, CodeQL, and container vulnerability scanning For detailed security information, see [Security Policy](SECURITY.md). ### Security Recommendations 1. **Enable Authentication for Public Access** - Set `MCP_AUTH_TOKEN` when using Tailscale Funnel or public internet - Generate strong tokens: `openssl rand -base64 32 | tr '+/' '-_' | tr -d '='` - Rotate tokens regularly 2. **Use HTTPS** - Tailscale provides automatic HTTPS certificates - Use reverse proxy (Nginx/Caddy) with Let's Encrypt for other deployments 3. **Prefer VPN Over Public Internet** - Tailscale Serve (VPN-only) is more secure than Funnel (public) - Authentication is optional within VPN but mandatory for public access 4. **Monitor Access** ```bash # Check for unauthorized access attempts docker compose logs mcp-server | grep "Unauthorized" ``` 5. **Network Isolation** - Keep ChromaDB on private network - Only expose MCP server to public internet --- ## Testing ### Local Testing ```bash # Health check curl http://localhost:3000/health # MCP tools list curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' # ChromaDB heartbeat curl http://localhost:3000/api/v2/heartbeat ``` ### Remote Testing (with authentication) ```bash # MCP endpoint (Bearer token) curl -X POST https://your-server.com/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' # MCP endpoint (Bearer token) curl -X POST "https://your-server.com/mcp" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' # ChromaDB REST API curl https://your-server.com/api/v2/heartbeat \ -H "Authorization: Bearer YOUR_TOKEN" # Swagger UI (browser) https://your-server.com/docs # send Authorization: Bearer YOUR_TOKEN header ``` --- ## Troubleshooting ### ChromaDB Connection Failed ```bash # Check if ChromaDB is running curl http://localhost:8000/api/v2/heartbeat # Start ChromaDB with Docker # WARNING: ChromaDB has no built-in authentication — do not publish on routable interface. # Bind to loopback only (127.0.0.1:8000:8000). Use MCP server as the authenticated gateway. docker run -d -p 127.0.0.1:8000:8000 chromadb/chroma:1.5.9 # Check MCP server logs docker compose logs mcp-server ``` ### MCP Server Not Responding ```bash # Check logs docker compose logs mcp-server # Check port conflicts lsof -i :3000 # Restart services docker compose restart ``` ### Claude Desktop Connection Issues 1. Restart Claude Desktop 2. Verify URL includes `/mcp` path 3. Confirm transport type is `streamableHttp` (not `sse`) 4. Check authentication token if enabled 5. For Custom Connector: Ensure Tailscale Funnel is active ### TLS Handshake Timeout on Local Network If you're connecting from the same local network as the server and using Tailscale Funnel HTTPS: **Problem**: TLS handshake fails with timeout when accessing `https://your-server.ts.net` from the same network. **Root cause**: Tailscale Funnel has issues with TLS termination when clients on the same LAN try to connect via the public Funnel domain. **Solution**: Use direct local network connection instead of Tailscale HTTPS: ```bash # Remove existing configuration claude mcp remove chromadb # Add with local IP address claude mcp add chromadb --transport http \ http://192.168.x.x:8080/mcp \ --header "Authorization: Bearer YOUR_TOKEN" # Or use hostname if DNS resolves claude mcp add chromadb --transport http \ http://server-hostname:8080/mcp \ --header "Authorization: Bearer YOUR_TOKEN" ``` **Verification**: ```bash # Test local network connection curl http://192.168.x.x:8080/health # Should return: {"status":"ok","service":"chroma-remote-mcp",...} ``` **Note**: External clients should continue using Tailscale Funnel HTTPS. This issue only affects clients on the same LAN as the server. ### Authentication Errors (401) ```bash # Verify MCP_AUTH_TOKEN is set docker compose exec mcp-server env | grep MCP_AUTH_TOKEN # Test without token (should fail with 401) curl -X POST https://your-server.com/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' # Test with correct token (should succeed) curl -X POST https://your-server.com/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` --- ## Development ### Building from Source ```bash # Clone repository git clone https://github.com/meloncafe/chromadb-remote-mcp.git cd chromadb-remote-mcp # Install dependencies yarn install # Development mode (auto-reload) yarn dev # Build TypeScript yarn build # Type check yarn type-check ``` ### Testing The project includes integration tests with Docker-based E2E validation: ```bash # Run all tests (starts services, runs tests, cleans up) yarn test # Run tests and keep containers running for debugging yarn test:keep # Manual test script with options ./scripts/test.sh --help ``` **Integration Test Coverage:** - ✅ Health check endpoint - ✅ Authentication (`Authorization: Bearer` MCP_AUTH_TOKEN; OAuth 2.1 / OIDC multi-provider) - ✅ MCP protocol (tools/list, tools/call) - ✅ ChromaDB REST API proxy - ✅ Collection CRUD operations - ✅ Rate limiting - ✅ Unauthorized access handling **Unit Tests:** ```bash # Run unit tests yarn test:unit # Run with watch mode yarn test:unit:watch # Run with coverage yarn test:unit:coverage # Run all tests (unit + integration) yarn test:all ``` **Unit Test Coverage:** - ✅ Authentication utilities (timing-safe comparison, buffer operations) - ✅ Input validation (collection names, document IDs, metadata) - ✅ Data processing (response formatting, JSON serialization) - ✅ Error message formatting See `__tests__/README.md` for detailed testing strategy. ### Code Quality & Coverage This project uses [Codecov](https://codecov.io/gh/meloncafe/chromadb-remote-mcp) for code coverage tracking and test analytics. ### Docker Development #### Local Build and Test ```bash # Build for local testing (single platform, loads to Docker) yarn docker:build:local # Or with script directly ./scripts/build.sh --platform linux/amd64 --load # Test the built image docker run -p 3000:3000 \ -e MCP_AUTH_TOKEN=test123 \ devsaurus/chromadb-remote-mcp:latest ``` #### Multi-Platform Build ```bash # Build for all platforms (amd64, arm64) yarn docker:build # Build with custom version ./scripts/build.sh --version 1.2.3 # Build with custom repository ./scripts/build.sh --repo myuser/my-mcp --version dev ``` #### Push to Docker Hub ```bash # Push latest tag yarn docker:push # Push specific version VERSION=1.2.3 yarn docker:push # Or with script directly ./scripts/build.sh --version 1.2.3 --push # With custom repository DOCKER_REPO=myuser/my-mcp ./scripts/build.sh --version 1.2.3 --push ``` **Environment Variables for Docker Scripts:** ```bash export DOCKER_REPO=myuser/my-mcp # Docker repository export VERSION=1.2.3 # Image version tag export DOCKER_USERNAME=myuser # For push authentication export DOCKER_PASSWORD=mytoken # Docker Hub token ``` ### Development Scripts All development scripts are located in `scripts/`: | Script | Purpose | Usage | | ------------ | ---------------------------- | --------------------------- | | `build.sh` | Build and push Docker images | `./scripts/build.sh --help` | | `test.sh` | Run integration tests | `./scripts/test.sh --help` | | `install.sh` | One-command installation | `curl ... \| bash` | **Quick Development Workflow:** ```bash # 1. Make code changes vim src/index.ts # 2. Test locally yarn dev # 3. Run integration tests yarn test # 4. Build Docker image yarn docker:build:local # 5. Test Docker image docker-compose up # 6. If all good, build multi-platform and push ./scripts/build.sh --version 1.2.3 --push ``` ### Project Structure ``` chromadb-remote-mcp/ ├── .github/ │ ├── ISSUE_TEMPLATE/ # GitHub issue templates │ └── workflows/ # GitHub Actions (publish-release, security-scan, chromadb-version-check.yml) ├── scripts/ │ ├── build.sh # Docker build and push script (multi-platform) │ ├── test.sh # Integration test runner │ └── install.sh # One-command installation ├── src/ │ ├── index.ts # Main server entry point │ ├── chroma-tools.ts # MCP tool definitions and handlers │ └── types.ts # TypeScript type definitions ├── docker-compose.yml # Production (prebuilt image) ├── docker-compose.dev.yml # Development (builds from source) ├── Dockerfile # MCP server Docker image ├── .env.example # Environment variables template ├── package.json # Node.js dependencies ├── tsconfig.json # TypeScript configuration ├── SECURITY.md # Security policy ├── CONTRIBUTING.md # Contribution guidelines ├── CODE_OF_CONDUCT.md # Code of conduct ├── CHANGELOG.md # Version history ├── CHANGELOG.ko.md # Version history (Korean) └── LICENSE # MIT license ``` --- ## v2.2.3 Release Notes — CVE-2026-45829 Security Hardening > **⚠️ Breaking changes** — operators upgrading from v2.2.2 or earlier must read this section. ### ChromaDB image version pinned (R4) All `docker-compose*.yml` files now pin `chromadb/chroma` to version `1.5.9@sha256:...`. Versions `1.0.0–1.5.8` are vulnerable to **CVE-2026-45829 (ChromaToast, CVSS 10.0)** — a pre-auth RCE via malicious embedding-function configuration. Do not downgrade the pin. A CI workflow (`.github/workflows/chromadb-version-check.yml`) fails the build if any docker-compose file references a version in the vulnerable range. ### Dev fail-open removed (R1, breaking) Previously, starting the server without `MCP_AUTH_TOKEN` or `OIDC_ISSUERS`/`OIDC_PRESET` would succeed silently in non-production environments. This behaviour is **removed**. The server now **refuses to start** unless at least one auth method is configured or `ALLOW_INSECURE_NO_AUTH=true` is explicitly set. **Migration:** - Production: set `MCP_AUTH_TOKEN` or configure OIDC. - Local dev: add `ALLOW_INSECURE_NO_AUTH=true` to your `.env`. ### ChromaDB REST catch-all proxy is now OFF by default (R3, breaking) The pass-through REST proxy (previously always mounted) is now **disabled unless** `CHROMA_REST_PROXY_ENABLED=true` is set. When disabled, all `/api/*` requests return 404. When enabled, the proxy enforces: - DNS-rebind protection (`validateOriginHeader`) — `Origin: evil.example` → 403 - Authentication (always required; `ALLOW_INSECURE_NO_AUTH` does **not** bypass the proxy) - Path filter: collection create/modify/delete and embedding-function endpoints are blocked (403) - Body sanitize: `configuration.embedding_function` in POST/PUT/PATCH body → 400 **Migration:** If you relied on direct `/api/v2/*` REST passthrough, set `CHROMA_REST_PROXY_ENABLED=true` and ensure authentication is configured. --- ## Contributing Contributions are welcome! Please feel free to submit issues and pull requests. 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request --- ## License [MIT License](LICENSE) --- ## Resources - [MCP Specification](https://modelcontextprotocol.io/specification/2025-06-18/) - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - [ChromaDB Documentation](https://docs.trychroma.com/) - [Tailscale Serve](https://tailscale.com/kb/1242/tailscale-serve/) - [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) - [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) --- ## Support If you encounter any issues or have questions, please [open an issue](https://github.com/meloncafe/chromadb-remote-mcp/issues). --- ## v2.0.0 Configuration > v2.0 introduces collection metadata schema v2, OAuth 2.1 OIDC, configurable embedding providers, and an optional reranker. See [MIGRATION.md](./MIGRATION.md) for the upgrade guide. ### Environment variables | Variable | Purpose | |----------|---------| | `EMBEDDING_PROVIDER` | `chromadb-default` (English-only, default) / `external` / `openai_compatible` / `gemini` / `voyage` | | `EMBEDDING_MODEL` | Provider-specific model id. Stored in collection metadata. | | `EMBEDDING_DIMENSIONS` | Vector dimensions. Required for external mode; Gemini accepts 768/1536/3072. | | `EMBEDDING_API_BASE` | OpenAI-compatible endpoint base URL (Ollama / TEI / Voyage / Together / vLLM). | | `EMBEDDING_API_KEY` | Bearer key for `openai_compatible` or `voyage` providers. | | `GEMINI_API_KEY` | Google AI Studio API key for the `gemini` provider. | | `CONFIDENCE_THRESHOLD` | Default `min_score` (0-1). Tool argument has priority. | | `RERANKER_API_BASE` | OpenAI-compatible `/rerank` endpoint. Reranker is fail-soft. | | `RERANKER_API_KEY` | Optional bearer key for the reranker. | | `RERANKER_MODEL` | Reranker model id (default `bge-reranker-v2-m3`). | | `OIDC_ISSUERS` | Comma-separated OIDC issuer URLs. | | `OIDC_PRESET` | Convenience preset names: `google,github,microsoft`. | | `OIDC_AUDIENCE` | Expected `aud` claim. | | `OIDC_SCOPES` | Comma-separated scopes for the Protected Resource Metadata. | | `OIDC_LOG_SUB_MODE` | `full` for raw `sub`, otherwise SHA-256 first 12 chars (default). | | `MCP_AUTH_TOKEN` | **Service-to-service / CI / internal scripts only.** Use OAuth for human users. Coexists with OIDC — either method accepts. | | `LEGACY_COLLECTION_COMPAT` | `true` to allow read-only access to legacy v1 collections. Writes are still rejected. | ### Recommended embedding + reranker combinations Verified locally on Korean RAG workloads (2026-05). Pick by priority: | Priority | Embedding | Reranker | Why | |----------|-----------|----------|-----| | Accuracy first (recommended) | `gemini` / `gemini-embedding-001` / 1536d | `cohere` / `rerank-multilingual-v3.0` | Gemini emits asymmetric query↔document vectors (`RETRIEVAL_QUERY`/`RETRIEVAL_DOCUMENT`, self-distance ≈ 0.21 in our test); Cohere reorders short KR question↔answer pairs cleanly. | | Cost-balanced | `voyage` / `voyage-3` / 1024d | `cohere` / `rerank-multilingual-v3.0` | Voyage embeddings are ~1/2.5 the cost of Gemini and still asymmetric (`input_type` query/document, self-distance ≈ 0.56). | | Minimum embedding cost | `openai_compatible` / `text-embedding-3-small` / 1536d | `cohere` / `rerank-multilingual-v3.0` | Cheapest hosted embedding; symmetric vectors are weaker on short KR queries, so the reranker is essential. | | Self-hosted / offline | `openai_compatible` (Ollama / TEI / vLLM) | TEI `bge-reranker-v2-m3` or similar | No external API; latency depends on local hardware. | Notes from the verification run: - Voyage `rerank-2` did NOT reorder the short KR question↔answer pair used in this test — keep Cohere as the rerank default for KR until your own corpus shows otherwise. - The reranker layer is fail-soft: leave `RERANKER_API_BASE` unset to disable reranking without code changes. - Set `CONFIDENCE_THRESHOLD` (or per-call `min_score`) to drop low-similarity hits; the server emits `confidence_gate: "no_confident_match"` when every result is filtered. ### Docker Compose snippet (Gemini + Google OAuth) ```yaml services: mcp-server: image: devsaurus/chromadb-remote-mcp:2.0.0 environment: EMBEDDING_PROVIDER: gemini EMBEDDING_MODEL: gemini-embedding-001 EMBEDDING_DIMENSIONS: "1536" GEMINI_API_KEY: ${GEMINI_API_KEY} OIDC_PRESET: google OIDC_AUDIENCE: ${OIDC_AUDIENCE} # e.g. your client_id CONFIDENCE_THRESHOLD: "0.55" RERANKER_API_BASE: "http://desktop-gpu.tail-xxxx.ts.net:8001" RERANKER_MODEL: bge-reranker-v2-m3 ``` ### OAuth flow 1. Configure your IdP (Google / GitHub / Microsoft) to issue tokens for an audience that matches `OIDC_AUDIENCE`. 2. Set `OIDC_PRESET=google` (or `OIDC_ISSUERS=...` for custom IdPs) and `OIDC_AUDIENCE=...`. 3. Clients send `Authorization: Bearer ` to `/mcp`. 4. 401 responses include `WWW-Authenticate: Bearer error="...", resource_metadata="/.well-known/oauth-protected-resource"` per RFC 9728. 5. `MCP_AUTH_TOKEN` remains valid alongside OAuth — recommended for non-interactive workloads. ### Reading legacy v1 collections Set `LEGACY_COLLECTION_COMPAT=true` to allow read-only access. Writes (`chroma_add_documents` / `update` / `delete`) on v1 collections still return `Error: Cannot write to legacy v1 collection`. See [MIGRATION.md](./MIGRATION.md).