# Zero-Config Architecture Plan
> **Goal:** Make doclea-mcp installable via `npx @doclea/mcp` with zero Docker requirement, while offering an optimized Docker-based installation for power users.
## Executive Summary
This document outlines the architectural changes required to transform doclea-mcp from a Docker-dependent MCP server to a truly zero-config solution that "just works" out of the box.
### Two Installation Paths
| Path | Command | Setup Time | Requirements | Best For |
|------|---------|------------|--------------|----------|
| **Simple** | `npx @doclea/mcp` | <30 seconds | Node.js/Bun | Testing, small projects |
| **Optimized** | `curl -fsSL https://doclea.ai/install.sh \| bash` | 3-5 minutes | Docker | Production, large codebases |
---
## Part 1: Embedded Vector Storage (sqlite-vec)
### Overview
Replace Qdrant with sqlite-vec for zero-config vector search. sqlite-vec is an SQLite extension that provides efficient vector similarity search without requiring a separate database server.
### Performance Comparison
| Metric | sqlite-vec | Qdrant |
|--------|-----------|--------|
| **Setup** | Zero config | Docker required |
| **Network overhead** | None (embedded) | HTTP/gRPC latency |
| **Query time (10k vectors)** | ~75ms | ~10-20ms |
| **Query time (100k vectors)** | ~150ms | ~30-50ms |
| **Memory footprint** | ~100-200MB | 500MB-1GB |
| **Best for** | <100k vectors | >100k vectors |
**Recommendation:** sqlite-vec is ideal for typical MCP usage (hundreds to low thousands of memories).
### Implementation
#### 1. Install sqlite-vec
```bash
bun add sqlite-vec
```
#### 2. Create SqliteVecStore
```typescript
// src/vectors/sqlite-vec.ts
import { Database } from "bun:sqlite";
import * as sqliteVec from "sqlite-vec";
import { platform } from "node:os";
import { existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import type { SearchFilters, VectorPayload, VectorSearchResult } from "@/types";
const VECTOR_SIZE = 384; // all-MiniLM-L6-v2 dimension
export class SqliteVecStore {
private db: Database;
private readonly tableName: string;
private initialized = false;
constructor(dbPath: string, tableName = "vector_memories") {
const dir = dirname(dbPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
// macOS: Set custom SQLite for extension support
if (platform() === "darwin") {
const brewPath = "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib";
const intelPath = "/usr/local/opt/sqlite/lib/libsqlite3.dylib";
if (existsSync(brewPath)) {
Database.setCustomSQLite(brewPath);
} else if (existsSync(intelPath)) {
Database.setCustomSQLite(intelPath);
}
}
this.db = new Database(dbPath, { create: true });
this.db.run("PRAGMA journal_mode = WAL");
this.tableName = tableName;
sqliteVec.load(this.db);
}
async initialize(): Promise<void> {
if (this.initialized) return;
// Create vector table
this.db.run(`
CREATE VIRTUAL TABLE IF NOT EXISTS ${this.tableName}
USING vec0(
id TEXT PRIMARY KEY,
embedding float[${VECTOR_SIZE}]
)
`);
// Create metadata table
this.db.run(`
CREATE TABLE IF NOT EXISTS ${this.tableName}_metadata (
id TEXT PRIMARY KEY,
memory_id TEXT NOT NULL,
type TEXT NOT NULL,
title TEXT NOT NULL,
tags TEXT NOT NULL,
related_files TEXT NOT NULL,
importance REAL NOT NULL,
payload TEXT NOT NULL
)
`);
// Create indexes
this.db.run(`CREATE INDEX IF NOT EXISTS idx_type ON ${this.tableName}_metadata(type)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_importance ON ${this.tableName}_metadata(importance)`);
this.initialized = true;
}
async upsert(id: string, vector: number[], payload: VectorPayload): Promise<string> {
await this.initialize();
const embedding = new Float32Array(vector);
this.db.run("BEGIN");
try {
this.db.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
this.db.run(`DELETE FROM ${this.tableName}_metadata WHERE id = ?`, [id]);
this.db.prepare(`
INSERT INTO ${this.tableName}(id, embedding) VALUES (?, vec_f32(?))
`).run(id, embedding.buffer);
this.db.prepare(`
INSERT INTO ${this.tableName}_metadata(
id, memory_id, type, title, tags, related_files, importance, payload
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id, payload.memoryId, payload.type, payload.title,
JSON.stringify(payload.tags), JSON.stringify(payload.relatedFiles),
payload.importance, JSON.stringify(payload)
);
this.db.run("COMMIT");
return id;
} catch (error) {
this.db.run("ROLLBACK");
throw error;
}
}
async search(
vector: number[],
filters?: SearchFilters,
limit: number = 10
): Promise<VectorSearchResult[]> {
await this.initialize();
const embedding = new Float32Array(vector);
// Build filter SQL
const conditions: string[] = [];
const params: unknown[] = [];
if (filters?.type) {
conditions.push("m.type = ?");
params.push(filters.type);
}
if (filters?.minImportance !== undefined) {
conditions.push("m.importance >= ?");
params.push(filters.minImportance);
}
const whereClause = conditions.length > 0
? `WHERE ${conditions.join(" AND ")}`
: "";
const query = `
SELECT v.id, v.distance, m.memory_id, m.payload
FROM ${this.tableName} v
INNER JOIN ${this.tableName}_metadata m ON v.id = m.id
${whereClause}
WHERE v.embedding MATCH vec_f32(?)
ORDER BY v.distance
LIMIT ?
`;
const rows = this.db.prepare(query).all(
...params, embedding.buffer, limit
) as Array<{ id: string; distance: number; memory_id: string; payload: string }>;
return rows.map((row) => ({
id: row.id,
memoryId: row.memory_id,
score: Math.max(0, 1 - row.distance / 2), // Convert distance to similarity
payload: JSON.parse(row.payload),
}));
}
async delete(id: string): Promise<boolean> {
await this.initialize();
const result = this.db.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
this.db.run(`DELETE FROM ${this.tableName}_metadata WHERE id = ?`, [id]);
return result.changes > 0;
}
close(): void {
this.db.close();
}
}
```
#### 3. Vector Store Abstraction
```typescript
// src/vectors/interface.ts
export interface VectorStore {
initialize(): Promise<void>;
upsert(id: string, vector: number[], payload: VectorPayload): Promise<string>;
search(vector: number[], filters?: SearchFilters, limit?: number): Promise<VectorSearchResult[]>;
delete(id: string): Promise<boolean>;
deleteByMemoryId(memoryId: string): Promise<boolean>;
getCollectionInfo(): Promise<{ vectorsCount: number; pointsCount: number }>;
close?(): void;
}
```
#### 4. Factory Pattern
```typescript
// src/vectors/factory.ts
export function createVectorStore(config: VectorConfig, projectPath: string): VectorStore {
if (config.provider === "qdrant") {
return new QdrantVectorStore(config);
}
return new SqliteVecStore(join(projectPath, config.dbPath || ".doclea/vectors.db"));
}
```
### macOS Compatibility Note
macOS ships with Apple's SQLite which doesn't support extensions. Users need vanilla SQLite:
```bash
brew install sqlite
```
The implementation auto-detects Homebrew paths and falls back with a clear error message.
---
## Part 2: Embedded Embeddings (Transformers.js)
### Overview
Replace TEI (Docker) with Transformers.js for in-process embedding generation. This eliminates the need for Docker and provides a seamless first-run experience.
### Model Selection
| Model | Size | Dimensions | Speed | Quality |
|-------|------|------------|-------|---------|
| **Xenova/all-MiniLM-L6-v2** (recommended) | 90MB | 384 | ~14k/sec | Good |
| Xenova/all-MiniLM-L12-v2 | 120MB | 768 | ~8k/sec | Better |
| Xenova/jina-embeddings-v2-small-en | 200MB | 512 | ~5k/sec | Best (8K context) |
**Recommendation:** all-MiniLM-L6-v2 offers the best balance of size, speed, and quality for code semantic search.
### Performance Comparison
| Metric | Transformers.js | TEI (Docker) |
|--------|-----------------|--------------|
| **Setup** | npm install | Docker + model download |
| **First run** | ~60s (90MB download) | 2-5 min (2GB Docker + model) |
| **Startup** | 2-5s (cached) | 20-30s (container + model) |
| **Memory** | 200-400MB | 500MB-1GB |
| **Latency** | 5-15ms | 10-20ms (+ HTTP overhead) |
| **Offline** | Yes (after first run) | Requires Docker daemon |
### Implementation
#### 1. Install Transformers.js
```bash
bun add @huggingface/transformers
```
#### 2. Create TransformersEmbeddingClient
```typescript
// src/embeddings/transformers.ts
import { pipeline, env, type FeatureExtractionPipeline } from "@huggingface/transformers";
import type { EmbeddingClient } from "./provider";
import { join } from "node:path";
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
export interface TransformersConfig {
model?: string;
cacheDir?: string;
device?: "auto" | "cpu" | "gpu";
quantized?: boolean;
}
export class TransformersEmbeddingClient implements EmbeddingClient {
private extractor: FeatureExtractionPipeline | null = null;
private initPromise: Promise<void> | null = null;
private readonly config: Required<TransformersConfig>;
constructor(config: TransformersConfig = {}) {
this.config = {
model: config.model ?? "Xenova/all-MiniLM-L6-v2",
cacheDir: config.cacheDir ?? this.getDefaultCacheDir(),
device: config.device ?? "auto",
quantized: config.quantized ?? true,
};
env.cacheDir = this.config.cacheDir;
env.allowLocalModels = true;
env.allowRemoteModels = true;
this.ensureCacheDir();
}
private getDefaultCacheDir(): string {
const xdgCache = process.env.XDG_CACHE_HOME;
const home = homedir();
if (xdgCache) return join(xdgCache, "doclea", "transformers");
if (process.platform === "win32") {
const localAppData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
return join(localAppData, "doclea", "transformers");
}
return join(home, ".cache", "doclea", "transformers");
}
private ensureCacheDir(): void {
if (!existsSync(this.config.cacheDir)) {
mkdirSync(this.config.cacheDir, { recursive: true });
}
}
private async initialize(): Promise<void> {
if (this.extractor) return;
if (this.initPromise) return this.initPromise;
this.initPromise = (async () => {
const isCached = this.isModelCached();
if (!isCached) {
console.error("[TransformersEmbedding] First run: Downloading model (~90MB)...");
console.error("[TransformersEmbedding] Model will be cached for future use");
}
const startTime = Date.now();
this.extractor = await pipeline("feature-extraction", this.config.model, {
device: this.config.device,
quantized: this.config.quantized,
});
console.error(`[TransformersEmbedding] Model ready in ${Date.now() - startTime}ms`);
})();
return this.initPromise;
}
private isModelCached(): boolean {
const modelName = this.config.model.replace("/", "_");
const modelPath = join(this.config.cacheDir, "models", modelName);
return existsSync(modelPath);
}
async embed(text: string): Promise<number[]> {
await this.initialize();
if (!this.extractor) throw new Error("Extractor not initialized");
const output = await this.extractor(text, {
pooling: "mean",
normalize: true,
});
return Array.from(output.data as Float32Array);
}
async embedBatch(texts: string[]): Promise<number[][]> {
await this.initialize();
if (!this.extractor) throw new Error("Extractor not initialized");
const output = await this.extractor(texts, {
pooling: "mean",
normalize: true,
});
const data = output.data as Float32Array;
const embedDim = 384;
const embeddings: number[][] = [];
for (let i = 0; i < texts.length; i++) {
const start = i * embedDim;
embeddings.push(Array.from(data.slice(start, start + embedDim)));
}
return embeddings;
}
getModelInfo() {
return {
model: this.config.model,
cacheDir: this.config.cacheDir,
dimensions: 384,
maxTokens: 512,
};
}
}
```
### Model Caching
**Cache locations:**
- Linux/Mac: `~/.cache/doclea/transformers`
- Windows: `%LOCALAPPDATA%\doclea\transformers`
**First-run experience:**
```
[TransformersEmbedding] First run: Downloading model (~90MB)...
[TransformersEmbedding] Model will be cached for future use
[TransformersEmbedding] Model ready in 58742ms
```
Subsequent runs: <5 seconds startup.
---
## Part 3: Configuration Schema
### Updated Config Types
```typescript
// src/types/index.ts
// Vector store providers
export const VectorConfigSchema = z.discriminatedUnion("provider", [
z.object({
provider: z.literal("sqlite-vec"),
dbPath: z.string().default(".doclea/vectors.db"),
}),
z.object({
provider: z.literal("qdrant"),
url: z.string(),
apiKey: z.string().optional(),
collectionName: z.string(),
}),
]);
// Embedding providers
export const EmbeddingConfigSchema = z.discriminatedUnion("provider", [
z.object({
provider: z.literal("transformers"),
model: z.string().default("Xenova/all-MiniLM-L6-v2"),
cacheDir: z.string().optional(),
quantized: z.boolean().default(true),
}),
z.object({
provider: z.literal("local"),
endpoint: z.string(),
}),
z.object({
provider: z.literal("openai"),
apiKey: z.string(),
model: z.string().default("text-embedding-3-small"),
}),
// ... other providers
]);
// Default config (zero-config)
export const DEFAULT_CONFIG: Config = {
embedding: { provider: "transformers" },
vector: { provider: "sqlite-vec" },
storage: { dbPath: ".doclea/local.db" },
};
```
### Auto-Detection Logic
```typescript
// src/config/loader.ts
export async function loadConfig(projectPath: string): Promise<Config> {
// 1. Check for explicit config file
const configPath = join(projectPath, ".doclea", "config.json");
if (existsSync(configPath)) {
return parseConfig(await readFile(configPath, "utf-8"));
}
// 2. Auto-detect available backends
const config = { ...DEFAULT_CONFIG };
// Check if Qdrant is running
try {
const response = await fetch("http://localhost:6333/readyz", { timeout: 1000 });
if (response.ok) {
config.vector = { provider: "qdrant", url: "http://localhost:6333", collectionName: "doclea_memories" };
}
} catch {
// Use sqlite-vec (default)
}
// Check if TEI is running
try {
const response = await fetch("http://localhost:8080/health", { timeout: 1000 });
if (response.ok) {
config.embedding = { provider: "local", endpoint: "http://localhost:8080" };
}
} catch {
// Use transformers.js (default)
}
return config;
}
```
---
## Part 4: Install Script (install.sh)
### Overview
A professional-grade install script similar to bun.sh, starship.rs, and rustup.
### Usage
```bash
curl -fsSL https://doclea.ai/install.sh | bash
```
### Features
1. **OS Detection:** Linux, macOS, Windows/WSL
2. **Prerequisites:** Git, Docker, Bun (auto-installs missing)
3. **Docker Services:** Qdrant + TEI with health checks
4. **Claude Config:** Auto-configures Claude Code MCP settings
5. **Progress Output:** Colored logs with clear status indicators
6. **Error Handling:** Graceful failures with rollback
7. **Uninstall:** Creates uninstall script for easy removal
### Script Location
```
scripts/install.sh
scripts/uninstall.sh
docs/INSTALLATION.md
```
### Key Sections
```bash
#!/usr/bin/env bash
set -euo pipefail
# 1. Environment setup and OS detection
# 2. Prerequisite checks (Docker, Bun)
# 3. Model download and caching
# 4. Docker Compose configuration
# 5. Service startup and health checks
# 6. npm package installation
# 7. Claude Code configuration
# 8. Uninstall script generation
# 9. Success summary with next steps
```
---
## Part 5: Implementation Plan
### Phase 1: Core Infrastructure (Priority: High)
| Task | Files | Estimated Effort |
|------|-------|-----------------|
| Add sqlite-vec dependency | `package.json` | Trivial |
| Implement SqliteVecStore | `src/vectors/sqlite-vec.ts` | Medium |
| Create VectorStore interface | `src/vectors/interface.ts` | Small |
| Add vector store factory | `src/vectors/factory.ts` | Small |
| Update tests for sqlite-vec | `src/__tests__/vectors/` | Medium |
### Phase 2: Transformers.js Integration (Priority: High)
| Task | Files | Estimated Effort |
|------|-------|-----------------|
| Add @huggingface/transformers | `package.json` | Trivial |
| Implement TransformersEmbeddingClient | `src/embeddings/transformers.ts` | Medium |
| Update embedding provider factory | `src/embeddings/provider.ts` | Small |
| Update tests for transformers | `src/__tests__/embeddings/` | Medium |
### Phase 3: Configuration (Priority: Medium)
| Task | Files | Estimated Effort |
|------|-------|-----------------|
| Update config schema | `src/types/index.ts` | Small |
| Implement auto-detection | `src/config/loader.ts` | Medium |
| Update DEFAULT_CONFIG | `src/config/defaults.ts` | Trivial |
### Phase 4: Install Scripts (Priority: Medium)
| Task | Files | Estimated Effort |
|------|-------|-----------------|
| Create install.sh | `scripts/install.sh` | Large |
| Create uninstall.sh | `scripts/uninstall.sh` | Small |
| Write installation docs | `docs/INSTALLATION.md` | Medium |
### Phase 5: Testing & Documentation (Priority: Medium)
| Task | Files | Estimated Effort |
|------|-------|-----------------|
| E2E tests for zero-config | `src/__tests__/integration/` | Medium |
| Update README.md | `README.md` | Medium |
| Add macOS SQLite notes | `docs/TROUBLESHOOTING.md` | Small |
---
## Part 6: Migration Path
### For New Users
```bash
# Zero-config (just works)
npx @doclea/mcp
```
### For Existing Docker Users
```json
// .doclea/config.json
{
"embedding": {
"provider": "local",
"endpoint": "http://localhost:8080"
},
"vector": {
"provider": "qdrant",
"url": "http://localhost:6333",
"collectionName": "doclea_memories"
}
}
```
### Auto-Detection
If Docker services are running, they're automatically used. Otherwise, embedded backends are used.
---
## Part 7: Testing Strategy
### Unit Tests
```typescript
// src/__tests__/vectors/sqlite-vec.test.ts
describe("SqliteVecStore", () => {
test("should upsert and search vectors", async () => {
const store = new SqliteVecStore(":memory:");
await store.initialize();
const vector = new Array(384).fill(0).map(() => Math.random());
await store.upsert("test-1", vector, mockPayload);
const results = await store.search(vector, undefined, 1);
expect(results[0].id).toBe("test-1");
expect(results[0].score).toBeGreaterThan(0.95);
});
});
```
```typescript
// src/__tests__/embeddings/transformers.test.ts
describe("TransformersEmbeddingClient", () => {
test("should generate embeddings", async () => {
const client = new TransformersEmbeddingClient();
const embedding = await client.embed("test text");
expect(embedding).toHaveLength(384);
expect(embedding.every(n => typeof n === "number")).toBe(true);
});
});
```
### Integration Tests
- Zero-config startup without Docker
- Auto-detection of running Docker services
- Graceful fallback when services unavailable
- Cross-platform (Linux, macOS, Windows)
---
## Appendix: File Structure After Implementation
```
packages/doclea-mcp/
├── src/
│ ├── embeddings/
│ │ ├── provider.ts # Factory + interface
│ │ ├── transformers.ts # NEW: Transformers.js client
│ │ ├── local-tei.ts # Existing TEI client
│ │ ├── openai.ts # Existing OpenAI client
│ │ └── ...
│ ├── vectors/
│ │ ├── interface.ts # NEW: VectorStore interface
│ │ ├── factory.ts # NEW: Factory function
│ │ ├── sqlite-vec.ts # NEW: sqlite-vec implementation
│ │ └── qdrant.ts # Existing Qdrant client
│ ├── config/
│ │ ├── loader.ts # Config loading + auto-detection
│ │ └── schema.ts # Updated Zod schemas
│ └── types/
│ └── index.ts # Updated type definitions
├── scripts/
│ ├── install.sh # NEW: Optimized install script
│ ├── uninstall.sh # NEW: Uninstall script
│ ├── setup-models.sh # Existing model setup
│ └── test-integration.sh # Existing test script
├── docs/
│ ├── INSTALLATION.md # NEW: Detailed install guide
│ ├── ZERO_CONFIG_ARCHITECTURE.md # This document
│ └── TROUBLESHOOTING.md # NEW: Common issues
├── package.json # Updated dependencies
└── README.md # Updated with new install options
```
---
## Summary
This architecture enables:
1. **True zero-config:** `npx @doclea/mcp` works immediately
2. **Offline capable:** After first model download
3. **Cross-platform:** Works on Linux, macOS, Windows
4. **Backward compatible:** Existing Docker setups continue to work
5. **Auto-detection:** Uses best available backend automatically
6. **Professional install:** curl script for power users
The embedded approach (sqlite-vec + Transformers.js) is sufficient for 99% of use cases while Docker remains available for large-scale deployments.