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
6 changes: 4 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
QDRANT_URL=http://qdrant:6333
# QDRANT_API_KEY= # not needed for local

# Default collection used by the MCP server (auto-created if missing)
# COLLECTION_NAME=my-collection # Use auto-detected default from .codebase/state.json
# Single unified collection for seamless cross-repo search
# Default: "codebase" - all your code in one collection for unified search
# This enables searching across multiple repos/workspaces without fragmentation
COLLECTION_NAME=codebase

# Embedding settings (FastEmbed model)
EMBEDDING_MODEL=BAAI/bge-base-en-v1.5
Expand Down
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Qdrant connection
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=
COLLECTION_NAME=my-collection
# Single unified collection for seamless cross-repo search (default: "codebase")
# Leave unset or use "codebase" for unified search across all your code
COLLECTION_NAME=codebase

# Embeddings
EMBEDDING_MODEL=BAAI/bge-base-en-v1.5
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ tests/.codebase/cache.json
tests/.codebase/state.json
/scripts/.codebase
/tests/.codebase
.claude/settings.local.json
.mcp.json
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Unified Context-Engine image for Kubernetes deployment
# Supports multiple roles: memory, indexer, watcher, llamacpp
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
WORK_ROOTS="/work,/app"

# Install OS dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*

# Install Python dependencies for all services
RUN pip install --no-cache-dir --upgrade \
qdrant-client \
fastembed \
watchdog \
onnxruntime \
tokenizers \
tree_sitter \
tree_sitter_languages \
mcp \
fastmcp

# Copy scripts for all services
COPY scripts /app/scripts

# Create directories
WORKDIR /work

# Expose all necessary ports
EXPOSE 8000 8001 8002 8003 18000 18001 18002 18003

# Default to memory server
CMD ["python", "/app/scripts/mcp_memory_server.py"]
91 changes: 88 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ INDEX_MICRO_CHUNKS=1 MAX_MICRO_CHUNKS_PER_FILE=200 make reset-dev-dual
```
- Default ports: Memory MCP :8000, Indexer MCP :8001, Qdrant :6333, llama.cpp :8080

**🎯 Seamless Setup Note:**
- The stack uses a **single unified `codebase` collection** by default
- All your code goes into one collection for seamless cross-repo search
- No per-workspace fragmentation - search across everything at once
- Health checks auto-detect and fix cache/collection sync issues
- Just run `make reset-dev-dual` on any machine and it works™

### Make targets: SSE, RMCP, and dual-compat
- Legacy SSE only (default):
- Ports: 8000 (/sse), 8001 (/sse)
Expand Down Expand Up @@ -96,9 +103,10 @@ INDEX_MICRO_CHUNKS=1 MAX_MICRO_CHUNKS_PER_FILE=200 make reset-dev-dual
GLM_MODEL=glm-4.6 # Optional, defaults to glm-4.6
```

5. **Custom collection name**:
5. **Collection name** (unified by default):
```bash
COLLECTION_NAME=my-project # Defaults to auto-detected repo name
COLLECTION_NAME=codebase # Default: single unified collection for all code
# Only change this if you need isolated collections per project
```

**After changing `.env`:**
Expand Down Expand Up @@ -280,7 +288,7 @@ Ports

| Name | Description | Default |
|------|-------------|---------|
| COLLECTION_NAME | Qdrant collection name used by both servers | my-collection |
| COLLECTION_NAME | Qdrant collection name (unified across all repos) | codebase |
| REPO_NAME | Logical repo tag stored in payload for filtering | auto-detect from git/folder |
| HOST_INDEX_PATH | Host path mounted at /work in containers | current repo (.) |
| QDRANT_URL | Qdrant base URL | container: http://qdrant:6333; local: http://localhost:6333 |
Expand Down Expand Up @@ -763,6 +771,50 @@ Notes:
- Named vector remains aligned with the MCP server (fast-bge-base-en-v1.5). If you change EMBEDDING_MODEL, run `make reindex` to recreate the collection.
- For very large repos, consider running `make index` on a schedule (or pre-commit) to keep Qdrant warm without full reingestion.

### Multi-repo indexing (unified search)

The stack uses a **single unified `codebase` collection** by default, making multi-repo search seamless:

**Index another repo into the same collection:**
```bash
# From your qdrant directory
make index-here HOST_INDEX_PATH=/path/to/other/repo REPO_NAME=other-repo

# Or with full control:
HOST_INDEX_PATH=/path/to/other/repo \
COLLECTION_NAME=codebase \
REPO_NAME=other-repo \
docker compose run --rm indexer --root /work
```

**What happens:**
- Files from the other repo get indexed into the unified `codebase` collection
- Each file is tagged with `metadata.repo = "other-repo"` for filtering
- Search across all repos by default, or filter by specific repo

**Search examples:**
```bash
# Search across all indexed repos
make hybrid QUERY="authentication logic"

# Filter by specific repo
python scripts/hybrid_search.py \
--query "authentication logic" \
--repo other-repo

# Filter by repo + language
python scripts/hybrid_search.py \
--query "authentication logic" \
--repo other-repo \
--language python
```

**Benefits:**
- One collection = unified search across all your code
- No fragmentation or collection management overhead
- Filter by repo when you need isolation
- All repos share the same vector space for better semantic search

### Multi-query re-ranker (no new deps)

- Run a fused query with several phrasings and metadata-aware boosts:
Expand Down Expand Up @@ -1296,6 +1348,39 @@ Client tips:

## Troubleshooting

### Collection Health & Cache Sync

The stack includes automatic health checks that detect and fix cache/collection sync issues:

**Check collection health:**
```bash
python scripts/collection_health.py --workspace . --collection codebase
```

**Auto-heal cache issues:**
```bash
python scripts/collection_health.py --workspace . --collection codebase --auto-heal
```

**What it detects:**
- Empty collection with cached files (cache thinks files are indexed but they're not)
- Significant mismatch between cached files and actual collection contents
- Missing metadata in collection points

**When to use:**
- After manually deleting collections
- If searches return no results despite indexing
- After Qdrant crashes or data loss
- When switching between collection names

**Automatic healing:**
- Health checks run automatically on watcher and indexer startup
- Cache is cleared when sync issues are detected
- Files are reindexed on next run

### General Issues

- If the MCP servers can’t reach Qdrant, confirm both containers are up: `make ps`.
- If the SSE port collides, change `FASTMCP_PORT` in `.env` and the mapped port in `docker-compose.yml`.
- If you customize tool descriptions, restart: `make restart`.
- If searches return no results, check collection health (see above).
199 changes: 199 additions & 0 deletions deploy/kubernetes/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Context-Engine Kubernetes Deployment Makefile

# Configuration
NAMESPACE ?= context-engine
IMAGE_REGISTRY ?= context-engine
IMAGE_TAG ?= latest

# Default target
.PHONY: help
help: ## Show this help message
@echo "Context-Engine Kubernetes Deployment Commands"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

# Prerequisites
.PHONY: check-kubectl
check-kubectl: ## Check if kubectl is available and cluster is accessible
@which kubectl > /dev/null || (echo "kubectl not found. Please install kubectl." && exit 1)
@kubectl cluster-info > /dev/null || (echo "Cannot connect to Kubernetes cluster." && exit 1)
@echo "✓ Kubernetes connection verified"

# Deployment targets
.PHONY: deploy
deploy: check-kubectl ## Deploy all Context-Engine services
./deploy.sh --namespace $(NAMESPACE) --registry $(IMAGE_REGISTRY) --tag $(IMAGE_TAG)

.PHONY: deploy-core
deploy-core: check-kubectl ## Deploy only core services (Qdrant + MCP servers)
@echo "Deploying core services..."
kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f qdrant.yaml
kubectl apply -f mcp-memory.yaml
kubectl apply -f mcp-indexer.yaml

.PHONY: deploy-full
deploy-full: check-kubectl ## Deploy all services including optional ones
./deploy.sh --namespace $(NAMESPACE) --registry $(IMAGE_REGISTRY) --tag $(IMAGE_TAG) --deploy-ingress

.PHONY: deploy-minimal
deploy-minimal: check-kubectl ## Deploy minimal setup (skip Llama.cpp and Ingress)
./deploy.sh --namespace $(NAMESPACE) --registry $(IMAGE_REGISTRY) --tag $(IMAGE_TAG) --skip-llamacpp

# Kustomize targets
.PHONY: kustomize-build
kustomize-build: ## Build manifests with Kustomize
kustomize build .

.PHONY: kustomize-apply
kustomize-apply: check-kubectl ## Apply manifests with Kustomize
kustomize build . | kubectl apply -f -

.PHONY: kustomize-delete
kustomize-delete: check-kubectl ## Delete manifests with Kustomize
kustomize build . | kubectl delete -f -

# Management targets
.PHONY: status
status: check-kubectl ## Show deployment status
@echo "=== Namespace Status ==="
kubectl get namespace $(NAMESPACE) || echo "Namespace $(NAMESPACE) not found"
@echo ""
@echo "=== Pods ==="
kubectl get pods -n $(NAMESPACE) -o wide || echo "No pods found"
@echo ""
@echo "=== Services ==="
kubectl get services -n $(NAMESPACE) || echo "No services found"
@echo ""
@echo "=== Deployments ==="
kubectl get deployments -n $(NAMESPACE) || echo "No deployments found"
@echo ""
@echo "=== StatefulSets ==="
kubectl get statefulsets -n $(NAMESPACE) || echo "No statefulsets found"
@echo ""
@echo "=== PersistentVolumeClaims ==="
kubectl get pvc -n $(NAMESPACE) || echo "No PVCs found"
@echo ""
@echo "=== Jobs ==="
kubectl get jobs -n $(NAMESPACE) || echo "No jobs found"

.PHONY: logs
logs: check-kubectl ## Show logs for all services
@echo "=== Qdrant Logs ==="
kubectl logs -f statefulset/qdrant -n $(NAMESPACE) --tail=50 || echo "Qdrant logs not available"

.PHONY: logs-service
logs-service: check-kubectl ## Show logs for specific service (usage: make logs-service SERVICE=mcp-memory)
@if [ -z "$(SERVICE)" ]; then echo "Usage: make logs-service SERVICE=<service-name>"; exit 1; fi
kubectl logs -f deployment/$(SERVICE) -n $(NAMESPACE) --tail=100 || kubectl logs -f statefulset/$(SERVICE) -n $(NAMESPACE) --tail=100 || kubectl logs -f job/$(SERVICE) -n $(NAMESPACE) --tail=100 || echo "Service $(SERVICE) not found"

.PHONY: shell
shell: check-kubectl ## Get a shell in a running pod (usage: make shell POD=mcp-memory-xxx)
@if [ -z "$(POD)" ]; then echo "Usage: make shell POD=<pod-name>"; echo "Available pods:"; kubectl get pods -n $(NAMESPACE); exit 1; fi
kubectl exec -it $(POD) -n $(NAMESPACE) -- /bin/bash || kubectl exec -it $(POD) -n $(NAMESPACE) -- /bin/sh

# Cleanup targets
.PHONY: cleanup
cleanup: check-kubectl ## Remove all Context-Engine resources
./cleanup.sh --namespace $(NAMESPACE)

.PHONY: clean-force
clean-force: check-kubectl ## Force cleanup without confirmation
./cleanup.sh --namespace $(NAMESPACE) --force

# Development targets
.PHONY: restart
restart: check-kubectl ## Restart all deployments
kubectl rollout restart deployment -n $(NAMESPACE)
kubectl rollout restart statefulset -n $(NAMESPACE)

.PHONY: restart-service
restart-service: check-kubectl ## Restart specific service (usage: make restart-service SERVICE=mcp-memory)
@if [ -z "$(SERVICE)" ]; then echo "Usage: make restart-service SERVICE=<service-name>"; exit 1; fi
kubectl rollout restart deployment/$(SERVICE) -n $(NAMESPACE) || kubectl rollout restart statefulset/$(SERVICE) -n $(NAMESPACE)

.PHONY: scale
scale: check-kubectl ## Scale a deployment (usage: make scale SERVICE=mcp-memory REPLICAS=3)
@if [ -z "$(SERVICE)" ] || [ -z "$(REPLICAS)" ]; then echo "Usage: make scale SERVICE=<service-name> REPLICAS=<number>"; exit 1; fi
kubectl scale deployment $(SERVICE) -n $(NAMESPACE) --replicas=$(REPLICAS)

# Port forwarding targets
.PHONY: port-forward
port-forward: check-kubectl ## Port forward all services
@echo "Opening port forwards in background..."
@kubectl port-forward -n $(NAMESPACE) service/qdrant 6333:6333 &
@kubectl port-forward -n $(NAMESPACE) service/mcp-memory 8000:8000 &
@kubectl port-forward -n $(NAMESPACE) service/mcp-indexer 8001:8001 &
@echo "Port forwards started. Use 'make stop-port-forward' to stop."

.PHONY: port-forward-service
port-forward-service: check-kubectl ## Port forward specific service (usage: make port-forward-service SERVICE=qdrant LOCAL=6333 REMOTE=6333)
@if [ -z "$(SERVICE)" ] || [ -z "$(LOCAL)" ] || [ -z "$(REMOTE)" ]; then echo "Usage: make port-forward-service SERVICE=<service-name> LOCAL=<local-port> REMOTE=<remote-port>"; exit 1; fi
kubectl port-forward -n $(NAMESPACE) service/$(SERVICE) $(LOCAL):$(REMOTE)

.PHONY: stop-port-forward
stop-port-forward: ## Stop all port forwards
pkill -f "kubectl port-forward" || echo "No port forwards found"

# Build and push targets
.PHONY: build-image
build-image: ## Build Docker image
docker build -t $(IMAGE_REGISTRY)/context-engine:$(IMAGE_TAG) ../../

.PHONY: push-image
push-image: build-image ## Push Docker image to registry
docker push $(IMAGE_REGISTRY)/context-engine:$(IMAGE_TAG)

# Test targets
.PHONY: test-connection
test-connection: check-kubectl ## Test connectivity to all services
@echo "Testing service connectivity..."
@echo "Qdrant:"
@kubectl run qdrant-test --image=curlimages/curl --rm -i --restart=Never -n $(NAMESPACE) -- curl -f http://qdrant.$(NAMESPACE).svc.cluster.local:6333/health || echo "Qdrant test failed"
@echo "MCP Memory:"
@kubectl run memory-test --image=curlimages/curl --rm -i --restart=Never -n $(NAMESPACE) -- curl -f http://mcp-memory.$(NAMESPACE).svc.cluster.local:18000/health || echo "MCP Memory test failed"
@echo "MCP Indexer:"
@kubectl run indexer-test --image=curlimages/curl --rm -i --restart=Never -n $(NAMESPACE) -- curl -f http://mcp-indexer.$(NAMESPACE).svc.cluster.local:18001/health || echo "MCP Indexer test failed"

# Configuration targets
.PHONY: show-config
show-config: ## Show current configuration
@echo "Configuration:"
@echo " NAMESPACE: $(NAMESPACE)"
@echo " IMAGE_REGISTRY: $(IMAGE_REGISTRY)"
@echo " IMAGE_TAG: $(IMAGE_TAG)"
@echo ""
@echo "Quick start commands:"
@echo " make deploy # Deploy all services"
@echo " make status # Show deployment status"
@echo " make logs-service SERVICE=mcp-memory # Show logs"
@echo " make cleanup # Remove everything"

.PHONY: show-urls
show-urls: check-kubectl ## Show access URLs for services
@echo "Service URLs (via NodePort):"
@echo " Qdrant: http://<node-ip>:30333"
@echo " MCP Memory (SSE): http://<node-ip>:30800"
@echo " MCP Indexer (SSE): http://<node-ip>:30802"
@echo " MCP Memory (HTTP): http://<node-ip>:30804"
@echo " MCP Indexer (HTTP): http://<node-ip>:30806"
@echo " Llama.cpp: http://<node-ip>:30808"
@echo ""
@echo "Service URLs (via port-forward):"
@echo " make port-forward # Then access via localhost ports"

# Advanced targets
.PHONY: watch-deployment
watch-deployment: check-kubectl ## Watch deployment progress
watch kubectl get pods,services,deployments -n $(NAMESPACE)

.PHONY: describe-service
describe-service: check-kubectl ## Describe a service (usage: make describe-service SERVICE=mcp-memory)
@if [ -z "$(SERVICE)" ]; then echo "Usage: make describe-service SERVICE=<service-name>"; echo "Available services:"; kubectl get services -n $(NAMESPACE); exit 1; fi
kubectl describe service $(SERVICE) -n $(NAMESPACE)

.PHONY: events
events: check-kubectl ## Show recent events
kubectl get events -n $(NAMESPACE) --sort-by=.metadata.creationTimestamp

Loading
Loading