diff --git a/.env.example b/.env.example
index 8eb0266a..e3779d05 100644
--- a/.env.example
+++ b/.env.example
@@ -90,3 +90,28 @@ SPLITTER_TYPE=ast
# Whether to use hybrid search mode. If true, it will use both dense vector and BM25; if false, it will use only dense vector search.
# HYBRID_MODE=true
+
+# =============================================================================
+# Docker Compose Configuration (only used by docker-compose.yml)
+# =============================================================================
+
+# MCP transport protocol
+# MCP_TRANSPORT=sse
+
+# MCP server port
+# MCP_PORT=8001
+
+# Path to the .context directory (default: $HOME/.context)
+# CONTEXT_DIR=${HOME}/.context
+
+# User/Group IDs for the claude-context container
+# UID=1000
+# GID=1000
+
+# Milvus exposed ports
+# MILVUS_PORT=19530
+# MILVUS_HEALTH_PORT=9091
+
+# Background sync configuration (0 = disabled)
+# SYNC_INITIAL_DELAY_MS=5000
+# SYNC_INTERVAL_MS=300000
diff --git a/.gitignore b/.gitignore
index 30159d04..6ff6e874 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ build/
.env.development.local
.env.production.local
+# Docker Compose local overrides
+docker-compose.override.yml
+
# Logs
npm-debug.log*
yarn-debug.log*
diff --git a/.npmrc b/.npmrc
index 9b21577b..4e2c36e6 100644
--- a/.npmrc
+++ b/.npmrc
@@ -15,3 +15,18 @@ cache-dir=~/.pnpm-cache
# Parallel execution optimization
child-concurrency=4
+
+# Allow native module build scripts
+onlyBuiltDependencies[]=esbuild
+onlyBuiltDependencies[]=tree-sitter
+onlyBuiltDependencies[]=tree-sitter-c-sharp
+onlyBuiltDependencies[]=tree-sitter-cpp
+onlyBuiltDependencies[]=tree-sitter-go
+onlyBuiltDependencies[]=tree-sitter-java
+onlyBuiltDependencies[]=tree-sitter-javascript
+onlyBuiltDependencies[]=tree-sitter-python
+onlyBuiltDependencies[]=tree-sitter-rust
+onlyBuiltDependencies[]=tree-sitter-scala
+onlyBuiltDependencies[]=tree-sitter-typescript
+onlyBuiltDependencies[]=tree-sitter-cli
+onlyBuiltDependencies[]=unrs-resolver
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..7001a366
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM node:20-slim AS builder
+WORKDIR /app
+COPY . .
+ENV CI=true
+RUN npm install -g pnpm && pnpm install --frozen-lockfile
+RUN pnpm --filter @zilliz/claude-context-core build \
+ && pnpm --filter @zilliz/claude-context-mcp build
+RUN pnpm --filter @zilliz/claude-context-mcp deploy --legacy /deploy
+
+FROM node:20-slim
+WORKDIR /app
+COPY --from=builder /deploy .
+
+ENV MCP_TRANSPORT=sse
+ENV MCP_PORT=8000
+EXPOSE 8000
+
+CMD ["node", "dist/index.js"]
\ No newline at end of file
diff --git a/README.md b/README.md
index ebacef1d..e9aac965 100644
--- a/README.md
+++ b/README.md
@@ -716,6 +716,305 @@ Check the `/examples` directory for complete usage examples:
---
+## π³ Self-Hosted with Docker Compose
+
+Run everything locally β no cloud accounts needed. This sets up Milvus (vector database) and the claude-context MCP server on your machine with a single command.
+
+### What you get
+
+- A **private, local** Milvus instance for vector storage (no Zilliz Cloud account required)
+- The claude-context MCP server running as a persistent SSE service
+- Semantic code search available to **any** MCP client (Claude Code, Copilot, Cursor, etc.)
+
+### Step 1: Prerequisites
+
+You need:
+
+- **Docker Engine 24+** with **Compose v2.23+** ([install guide](https://docs.docker.com/engine/install/))
+- An **embedding API key** (e.g., [OpenAI](https://platform.openai.com/api-keys) β starts with `sk-`)
+- **~10 GB free disk space** for Milvus data
+
+Verify Docker is ready:
+
+```bash
+docker compose version # Should show v2.23.0 or higher
+```
+
+### Step 2: Clone the repository
+
+```bash
+git clone https://github.com/zilliztech/claude-context.git
+cd claude-context
+```
+
+### Step 3: Create your configuration
+
+Copy the example `.env` and edit it with your API key:
+
+```bash
+mkdir -p ~/.context
+cp .env.example ~/.context/.env
+```
+
+Open `~/.context/.env` in your editor and set **at minimum** these two values:
+
+```ini
+# ~/.context/.env
+
+OPENAI_API_KEY=sk-your-actual-openai-key-here
+MILVUS_ADDRESS=localhost:19530
+```
+
+> **Using Ollama instead of OpenAI?** Set `EMBEDDING_PROVIDER=Ollama`, `OLLAMA_HOST=http://host.docker.internal:11434`, and comment out `OPENAI_API_KEY`. See [.env.example](.env.example) for all providers.
+
+### Step 4: Tell Docker where your code lives
+
+The MCP server needs read access to your codebases. Create a `docker-compose.override.yml` from the template:
+
+```bash
+cp docker-compose.override.example.yml docker-compose.override.yml
+```
+
+Edit `docker-compose.override.yml` with your actual project paths. **Important**: source and destination must be the same path (the indexer stores absolute paths):
+
+```yaml
+# docker-compose.override.yml
+services:
+ claude-context:
+ volumes:
+ - /home/youruser/projects/myapp:/home/youruser/projects/myapp:ro
+ - /home/youruser/projects/backend:/home/youruser/projects/backend:ro
+```
+
+> On **macOS**, paths look like `/Users/youruser/projects/myapp`.
+
+### Step 5: Start the services
+
+```bash
+docker compose up -d
+```
+
+The first run will:
+1. Build the claude-context image (~2 min)
+2. Pull the Milvus image (~1 GB download)
+3. Start both services
+
+Wait for Milvus to become healthy (~30β90 seconds):
+
+```bash
+docker compose ps
+```
+
+You should see:
+
+```
+NAME STATUS
+milvus-standalone Up (healthy)
+claude-context-mcp Up
+```
+
+### Step 6: Verify the MCP server is reachable
+
+```bash
+curl -s http://localhost:8001/sse
+```
+
+If it hangs waiting for events β that's correct! The SSE endpoint is streaming. Press `Ctrl+C` to stop.
+
+### Step 7: Connect your AI coding tool
+
+The self-hosted server uses **SSE transport** on `http://localhost:8001/sse`. Configure your tool:
+
+
+Claude Code
+
+```bash
+claude mcp add --transport sse claude-context http://localhost:8001/sse
+```
+
+Verify it was added:
+
+```bash
+claude mcp list
+```
+
+
+
+
+VS Code (Copilot Chat / GitHub Copilot)
+
+Add to your VS Code settings (`.vscode/mcp.json` in your project, or user settings):
+
+```json
+{
+ "servers": {
+ "claude-context": {
+ "type": "sse",
+ "url": "http://localhost:8001/sse"
+ }
+ }
+}
+```
+
+Restart VS Code. You should see claude-context tools available in Copilot Chat (agent mode).
+
+
+
+
+Cursor
+
+Add to `~/.cursor/mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "claude-context": {
+ "type": "sse",
+ "url": "http://localhost:8001/sse"
+ }
+ }
+}
+```
+
+
+
+
+Claude Desktop
+
+Add to your Claude Desktop config (`claude_desktop_config.json`):
+
+```json
+{
+ "mcpServers": {
+ "claude-context": {
+ "type": "sse",
+ "url": "http://localhost:8001/sse"
+ }
+ }
+}
+```
+
+
+
+
+Any other MCP client
+
+Use SSE transport pointed at:
+
+```
+http://localhost:8001/sse
+```
+
+No API keys or environment variables needed on the client side β all configuration lives in `~/.context/.env`.
+
+
+
+### Step 8: Index your codebase
+
+Open your AI tool and ask it to index your project:
+
+```
+Index the codebase at /home/youruser/projects/myapp
+```
+
+> **Note**: Use the **absolute path** that matches the volume mount from Step 4.
+
+The indexer will parse your code into an AST, generate embeddings, and store them in Milvus. Progress is reported in real time. A typical 100K-line codebase takes 2β5 minutes.
+
+### Step 9: Test it
+
+Once indexing completes, try a semantic search:
+
+```
+Search for functions that handle authentication in /home/youruser/projects/myapp
+```
+
+You should get back relevant code snippets with file paths, line numbers, and similarity scores.
+
+Check the indexing status at any time with:
+
+```
+Check the indexing status
+```
+
+### Troubleshooting
+
+| Problem | Solution |
+|---------|----------|
+| `docker compose ps` shows Milvus unhealthy | Wait 90 seconds for startup. Check `docker compose logs milvus` |
+| `curl localhost:8001/sse` connection refused | Check `docker compose logs claude-context` β likely a missing `.env` |
+| Indexing fails with "connection refused" | Verify `MILVUS_ADDRESS=localhost:19530` in `~/.context/.env` |
+| "No such file or directory" during index | The path must match a volume mount in `docker-compose.override.yml` |
+| Permission denied errors | Set `UID` and `GID` in a `.env` file next to `docker-compose.yml`: `UID=1000` and `GID=1000` (use `id -u` and `id -g` to find yours) |
+
+### Configuration reference
+
+All variables have sensible defaults. Override in a `.env` file next to `docker-compose.yml`:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `MCP_PORT` | `8001` | SSE server port |
+| `MCP_TRANSPORT` | `sse` | Transport protocol |
+| `CONTEXT_DIR` | `$HOME/.context` | Path to .context directory |
+| `UID` / `GID` | `1000` | Container user/group IDs |
+| `MILVUS_PORT` | `19530` | Milvus gRPC port |
+| `MILVUS_HEALTH_PORT` | `9091` | Milvus health check port |
+
+### Architecture
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββ
+β Docker Compose β
+β β
+β ββββββββββββ bridge βββββββββββββ β
+β β Milvus βββββnetworkβββββΊβ claude- β β
+β β (vector β milvus:19530 β context β β
+β β db) β β (MCP) β β
+β ββββββ¬ββββββ βββββββ¬ββββββ β
+β β β β
+β :19530 :9091 :8001 β
+βββββββββΌββββββββββββββββββββββββββΌβββββββββββββ
+ β β
+ host ports host port βββ Claude Code
+ (optional) (SSE) βββ Copilot
+ βββ Cursor
+```
+
+- **Bridge network**: Services communicate internally via DNS. No `network_mode: host` needed.
+- **Milvus configs**: Defined inline in `docker-compose.yml` via `configs:`. No external config files.
+- **Data**: Named volume `milvus_data` (Docker-managed, persists across restarts).
+- **Runtime state**: `~/.context/` bind-mounted into claude-context.
+
+### Daily usage
+
+```bash
+docker compose up -d # Start all services
+docker compose down # Stop all (data preserved in volume)
+docker compose restart claude-context # Restart MCP only
+docker compose build claude-context # Rebuild image (after git pull)
+docker compose up -d --build claude-context # Rebuild + restart
+docker compose logs -f # Follow all logs
+```
+
+### Migrating from standalone containers
+
+If you previously ran Milvus via `standalone_embed.sh`:
+
+```bash
+# 1. Stop old containers
+docker stop claude-context-mcp milvus-standalone
+docker rm claude-context-mcp milvus-standalone
+
+# 2. Migrate data to named volume (idempotent β skips if volume already has data)
+MILVUS_LEGACY_DATA=/path/to/old/volumes/milvus \
+ docker compose --profile migrate run --rm milvus-migrate
+
+# 3. Start
+docker compose up -d
+```
+
+---
+
## π€ Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to get started.
diff --git a/docker-compose.override.example.yml b/docker-compose.override.example.yml
new file mode 100644
index 00000000..5113f105
--- /dev/null
+++ b/docker-compose.override.example.yml
@@ -0,0 +1,11 @@
+# Copy to docker-compose.override.yml and adjust paths.
+# Each codebase mount must use the SAME path as source and destination
+# because the indexer stores absolute paths.
+#
+# Example:
+# - /home/user/projects/myapp:/home/user/projects/myapp:ro
+services:
+ claude-context:
+ volumes:
+ - /path/to/codebase1:/path/to/codebase1:ro
+ - /path/to/codebase2:/path/to/codebase2:ro
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..6dc16c2c
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,89 @@
+configs:
+ embed_etcd:
+ content: |
+ listen-client-urls: http://0.0.0.0:2379
+ advertise-client-urls: http://0.0.0.0:2379
+ quota-backend-bytes: 4294967296
+ auto-compaction-mode: revision
+ auto-compaction-retention: '1000'
+ milvus_user:
+ content: |
+ # Extra config to override default milvus.yaml
+
+services:
+ milvus:
+ container_name: milvus-standalone
+ image: milvusdb/milvus:v2.6.0
+ command: milvus run standalone
+ security_opt:
+ - seccomp:unconfined
+ environment:
+ ETCD_USE_EMBED: "true"
+ ETCD_DATA_DIR: /var/lib/milvus/etcd
+ ETCD_CONFIG_PATH: /milvus/configs/embedEtcd.yaml
+ COMMON_STORAGETYPE: local
+ DEPLOY_MODE: STANDALONE
+ configs:
+ - source: embed_etcd
+ target: /milvus/configs/embedEtcd.yaml
+ - source: milvus_user
+ target: /milvus/configs/user.yaml
+ volumes:
+ - milvus_data:/var/lib/milvus
+ ports:
+ - "${MILVUS_PORT:-19530}:19530"
+ - "${MILVUS_HEALTH_PORT:-9091}:9091"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
+ interval: 30s
+ start_period: 90s
+ timeout: 20s
+ retries: 3
+ restart: unless-stopped
+
+ claude-context:
+ container_name: claude-context-mcp
+ image: claude-context-mcp
+ build: .
+ user: "${UID:-1000}:${GID:-1000}"
+ env_file:
+ - ${CONTEXT_DIR:-${HOME}/.context}/.env
+ environment:
+ HOME: /home/node
+ MCP_TRANSPORT: ${MCP_TRANSPORT:-sse}
+ MCP_PORT: "${MCP_PORT:-8001}"
+ MILVUS_ADDRESS: milvus:19530
+ volumes:
+ - ${CONTEXT_DIR:-${HOME}/.context}:/home/node/.context
+ ports:
+ - "${MCP_PORT:-8001}:${MCP_PORT:-8001}"
+ depends_on:
+ milvus:
+ condition: service_healthy
+ restart: unless-stopped
+
+ milvus-migrate:
+ image: alpine
+ volumes:
+ - ${MILVUS_LEGACY_DATA:-/tmp}:/src:ro
+ - milvus_data:/dest
+ entrypoint: ["sh", "-c"]
+ command:
+ - |
+ if [ -n "$$(ls -A /dest 2>/dev/null)" ]; then
+ echo "Volume already has data, skipping."
+ exit 0
+ fi
+ if [ ! -d /src/etcd ]; then
+ echo "ERROR: No Milvus data found in /src."
+ echo "Usage: MILVUS_LEGACY_DATA=/path/to/old/milvus docker compose --profile migrate run --rm milvus-migrate"
+ exit 1
+ fi
+ echo "Migrating Milvus data from /src..."
+ cp -a /src/. /dest/
+ echo "Migration complete."
+ profiles:
+ - migrate
+
+volumes:
+ milvus_data:
diff --git a/package.json b/package.json
index 4fe5a792..dd504ca4 100644
--- a/package.json
+++ b/package.json
@@ -47,5 +47,22 @@
"url": "https://github.com/zilliztech/claude-context.git"
},
"license": "MIT",
- "author": "Cheney Zhang <277584121@qq.com>"
+ "author": "Cheney Zhang <277584121@qq.com>",
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "esbuild",
+ "tree-sitter",
+ "tree-sitter-c-sharp",
+ "tree-sitter-cli",
+ "tree-sitter-cpp",
+ "tree-sitter-go",
+ "tree-sitter-java",
+ "tree-sitter-javascript",
+ "tree-sitter-python",
+ "tree-sitter-rust",
+ "tree-sitter-scala",
+ "tree-sitter-typescript",
+ "unrs-resolver"
+ ]
+ }
}
\ No newline at end of file
diff --git a/packages/mcp/README.md b/packages/mcp/README.md
index 4a562af5..f6b7604a 100644
--- a/packages/mcp/README.md
+++ b/packages/mcp/README.md
@@ -608,7 +608,7 @@ For LangChain/LangGraph integration examples, see [this example](https://github.
Other MCP Clients
-The server uses stdio transport and follows the standard MCP protocol. It can be integrated with any MCP-compatible client by running:
+The server uses stdio transport by default and follows the standard MCP protocol. It can be integrated with any MCP-compatible client by running:
```bash
npx @zilliz/claude-context-mcp@latest
@@ -616,6 +616,54 @@ npx @zilliz/claude-context-mcp@latest
+### SSE Transport (Shared Server)
+
+By default, each MCP client spawns its own `claude-context-mcp` process via stdio. When using multiple clients (e.g., multiple IDE tabs), this leads to duplicated processes, each consuming CPU and memory independently.
+
+**SSE transport** lets you run a **single shared server** that multiple clients connect to over HTTP, eliminating resource duplication.
+
+#### Start the SSE server
+
+```bash
+# Via CLI flags
+npx @zilliz/claude-context-mcp@latest --transport sse --port 8000
+
+# Via environment variables
+MCP_TRANSPORT=sse MCP_PORT=8000 npx @zilliz/claude-context-mcp@latest
+```
+
+#### Connect clients via SSE
+
+Configure your MCP client to connect to the running SSE server instead of spawning a new process:
+
+```json
+{
+ "mcpServers": {
+ "claude-context": {
+ "type": "sse",
+ "url": "http://localhost:8000/sse"
+ }
+ }
+}
+```
+
+#### Docker
+
+```bash
+docker build -t claude-context-mcp .
+docker run -d --name claude-context-mcp \
+ -p 8000:8000 \
+ -e EMBEDDING_PROVIDER=OpenAI \
+ -e OPENAI_API_KEY=your-key \
+ -e MILVUS_TOKEN=your-token \
+ claude-context-mcp
+```
+
+| Environment Variable | Description | Default |
+| -------------------- | -------------------------------- | ------- |
+| `MCP_TRANSPORT` | Transport mode: `stdio` or `sse` | `stdio` |
+| `MCP_PORT` | Port for SSE server | `8000` |
+
## Features
- π **MCP Protocol Compliance**: Full compatibility with MCP-enabled AI assistants and agents
diff --git a/packages/mcp/src/config.ts b/packages/mcp/src/config.ts
index 428f9474..e2d1098e 100644
--- a/packages/mcp/src/config.ts
+++ b/packages/mcp/src/config.ts
@@ -3,6 +3,9 @@ import { envManager } from "@zilliz/claude-context-core";
export interface ContextMcpConfig {
name: string;
version: string;
+ // Transport configuration
+ transport: 'stdio' | 'sse';
+ port: number;
// Embedding provider configuration
embeddingProvider: 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama';
embeddingModel: string;
@@ -102,6 +105,16 @@ export function getEmbeddingModelForProvider(provider: string): string {
}
}
+// Helper to parse CLI arguments (--key value pairs)
+function getCliArg(name: string): string | undefined {
+ const args = process.argv.slice(2);
+ const idx = args.indexOf(`--${name}`);
+ if (idx !== -1 && idx + 1 < args.length) {
+ return args[idx + 1];
+ }
+ return undefined;
+}
+
export function createMcpConfig(): ContextMcpConfig {
// Debug: Print all environment variables related to Context
console.log(`[DEBUG] π Environment Variables Debug:`);
@@ -113,9 +126,16 @@ export function createMcpConfig(): ContextMcpConfig {
console.log(`[DEBUG] MILVUS_ADDRESS: ${envManager.get('MILVUS_ADDRESS') || 'NOT SET'}`);
console.log(`[DEBUG] NODE_ENV: ${envManager.get('NODE_ENV') || 'NOT SET'}`);
+ // Transport config: CLI args take priority over env vars
+ const transportArg = getCliArg('transport') || envManager.get('MCP_TRANSPORT') || 'stdio';
+ const portArg = getCliArg('port') || envManager.get('MCP_PORT') || '8000';
+
const config: ContextMcpConfig = {
name: envManager.get('MCP_SERVER_NAME') || "Context MCP Server",
version: envManager.get('MCP_SERVER_VERSION') || "1.0.0",
+ // Transport configuration
+ transport: transportArg === 'sse' ? 'sse' : 'stdio',
+ port: parseInt(portArg, 10) || 8000,
// Embedding provider configuration
embeddingProvider: (envManager.get('EMBEDDING_PROVIDER') as 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama') || 'OpenAI',
embeddingModel: getEmbeddingModelForProvider(envManager.get('EMBEDDING_PROVIDER') || 'OpenAI'),
@@ -141,6 +161,7 @@ export function logConfigurationSummary(config: ContextMcpConfig): void {
console.log(`[MCP] π Starting Context MCP Server`);
console.log(`[MCP] Configuration Summary:`);
console.log(`[MCP] Server: ${config.name} v${config.version}`);
+ console.log(`[MCP] Transport: ${config.transport}${config.transport === 'sse' ? ` (port ${config.port})` : ''}`);
console.log(`[MCP] Embedding Provider: ${config.embeddingProvider}`);
console.log(`[MCP] Embedding Model: ${config.embeddingModel}`);
console.log(`[MCP] Milvus Address: ${config.milvusAddress || (config.milvusToken ? '[Auto-resolve from token]' : '[Not configured]')}`);
@@ -179,8 +200,13 @@ Usage: npx @zilliz/claude-context-mcp@latest [options]
Options:
--help, -h Show this help message
+ --transport Transport mode (default: stdio)
+ --port Port for SSE transport (default: 8000)
Environment Variables:
+ MCP_TRANSPORT Transport mode: stdio or sse (default: stdio)
+ MCP_PORT Port for SSE transport (default: 8000)
+
MCP_SERVER_NAME Server name
MCP_SERVER_VERSION Server version
@@ -221,5 +247,11 @@ Examples:
# Start MCP server with Ollama and specific model (using EMBEDDING_MODEL)
EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_TOKEN=your-token npx @zilliz/claude-context-mcp@latest
+
+ # Start MCP server with SSE transport (shared server for multiple clients)
+ npx @zilliz/claude-context-mcp@latest --transport sse --port 8000
+
+ # SSE transport via environment variables
+ MCP_TRANSPORT=sse MCP_PORT=8000 npx @zilliz/claude-context-mcp@latest
`);
}
\ No newline at end of file
diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts
index 8c4c3b28..8b5976a5 100644
--- a/packages/mcp/src/index.ts
+++ b/packages/mcp/src/index.ts
@@ -17,10 +17,15 @@ console.warn = (...args: any[]) => {
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
ListToolsRequestSchema,
- CallToolRequestSchema
+ CallToolRequestSchema,
+ isInitializeRequest,
} from "@modelcontextprotocol/sdk/types.js";
+import { createServer, IncomingMessage, ServerResponse } from "node:http";
+import { randomUUID } from "node:crypto";
import { Context } from "@zilliz/claude-context-core";
import { MilvusVectorDatabase } from "@zilliz/claude-context-core";
@@ -31,26 +36,31 @@ import { SnapshotManager } from "./snapshot.js";
import { SyncManager } from "./sync.js";
import { ToolHandlers } from "./handlers.js";
+/** Read and JSON-parse the request body from an IncomingMessage. */
+function readBody(req: IncomingMessage): Promise> {
+ return new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
+ req.on('end', () => {
+ try {
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ req.on('error', reject);
+ });
+}
+
class ContextMcpServer {
- private server: Server;
+ private config: ContextMcpConfig;
private context: Context;
private snapshotManager: SnapshotManager;
private syncManager: SyncManager;
private toolHandlers: ToolHandlers;
constructor(config: ContextMcpConfig) {
- // Initialize MCP server
- this.server = new Server(
- {
- name: config.name,
- version: config.version
- },
- {
- capabilities: {
- tools: {}
- }
- }
- );
+ this.config = config;
// Initialize embedding provider
console.log(`[EMBEDDING] Initializing embedding provider: ${config.embeddingProvider}`);
@@ -78,11 +88,30 @@ class ContextMcpServer {
// Load existing codebase snapshot on startup
this.snapshotManager.loadCodebaseSnapshot();
+ }
- this.setupTools();
+ /**
+ * Creates a new Server instance with tool handlers registered.
+ * Each client session (SSE or Streamable HTTP) needs its own Server
+ * instance because Protocol.connect() only supports one transport at a time.
+ */
+ private createServer(): Server {
+ const server = new Server(
+ {
+ name: this.config.name,
+ version: this.config.version
+ },
+ {
+ capabilities: {
+ tools: {}
+ }
+ }
+ );
+ this.setupTools(server);
+ return server;
}
- private setupTools() {
+ private setupTools(server: Server) {
const index_description = `
Index a codebase directory to enable semantic search using a configurable code splitter.
@@ -117,7 +146,7 @@ This tool is versatile and can be used before completing various tasks to retrie
`;
// Define available tools
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
@@ -226,7 +255,7 @@ This tool is versatile and can be used before completing various tasks to retrie
});
// Handle tool execution
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
+ server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const { name, arguments: args } = request.params;
switch (name) {
@@ -247,14 +276,175 @@ This tool is versatile and can be used before completing various tasks to retrie
async start() {
console.log('[SYNC-DEBUG] MCP server start() method called');
- console.log('Starting Context MCP server...');
-
- const transport = new StdioServerTransport();
- console.log('[SYNC-DEBUG] StdioServerTransport created, attempting server connection...');
-
- await this.server.connect(transport);
- console.log("MCP server started and listening on stdio.");
- console.log('[SYNC-DEBUG] Server connection established successfully');
+ console.log(`Starting Context MCP server (transport: ${this.config.transport})...`);
+
+ if (this.config.transport === 'sse') {
+ type TransportEntry = SSEServerTransport | StreamableHTTPServerTransport;
+ const sessions = new Map();
+ const sseResponses = new Map(); // raw res refs for heartbeat
+
+ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
+ const url = new URL(req.url || '', `http://localhost:${this.config.port}`);
+ const pathname = url.pathname;
+
+ try {
+ // ββ Streamable HTTP (protocol 2025-03-26) βββββββββββββ
+ if (pathname === '/mcp') {
+ if (req.method === 'POST') {
+ let body: Record;
+ try {
+ body = await readBody(req);
+ } catch {
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
+ res.end('Invalid JSON');
+ return;
+ }
+
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
+ let transport = sessionId ? sessions.get(sessionId) : undefined;
+
+ if (transport && !(transport instanceof StreamableHTTPServerTransport)) {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ jsonrpc: '2.0',
+ error: { code: -32000, message: 'Session uses a different transport protocol' },
+ id: null,
+ }));
+ return;
+ }
+
+ if (!transport && isInitializeRequest(body)) {
+ const newTransport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: () => randomUUID(),
+ onsessioninitialized: (sid: string) => {
+ console.log(`[HTTP] Streamable HTTP session initialized: ${sid}`);
+ sessions.set(sid, newTransport);
+ },
+ });
+ newTransport.onclose = () => {
+ const sid = newTransport.sessionId;
+ if (sid) {
+ console.log(`[HTTP] Streamable HTTP session closed: ${sid}`);
+ sessions.delete(sid);
+ }
+ };
+ const server = this.createServer();
+ await server.connect(newTransport);
+ transport = newTransport;
+ }
+
+ if (transport instanceof StreamableHTTPServerTransport) {
+ await transport.handleRequest(req, res, body);
+ } else {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ jsonrpc: '2.0',
+ error: { code: -32000, message: 'Bad Request: No valid session' },
+ id: null,
+ }));
+ }
+ } else if (req.method === 'GET' || req.method === 'DELETE') {
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
+ const transport = sessionId ? sessions.get(sessionId) : undefined;
+ if (transport instanceof StreamableHTTPServerTransport) {
+ await transport.handleRequest(req, res);
+ } else {
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
+ res.end('Session not found');
+ }
+ } else {
+ res.writeHead(405, { 'Content-Type': 'text/plain' });
+ res.end('Method not allowed');
+ }
+ }
+
+ // ββ Legacy SSE (deprecated protocol 2024-11-05) βββββββ
+ else if (req.method === 'GET' && pathname === '/sse') {
+ console.log(`[SSE] New client connection`);
+ const transport = new SSEServerTransport('/message', res);
+ sessions.set(transport.sessionId, transport);
+ sseResponses.set(transport.sessionId, res);
+
+ const server = this.createServer();
+
+ transport.onclose = () => {
+ console.log(`[SSE] Client disconnected (session: ${transport.sessionId})`);
+ sessions.delete(transport.sessionId);
+ sseResponses.delete(transport.sessionId);
+ };
+
+ // Redundant close handler on raw response for extra safety
+ res.on('close', () => {
+ sessions.delete(transport.sessionId);
+ sseResponses.delete(transport.sessionId);
+ });
+
+ await server.connect(transport);
+ console.log(`[SSE] Client connected (session: ${transport.sessionId}, active: ${sessions.size})`);
+ }
+
+ else if (req.method === 'POST' && pathname === '/message') {
+ const sessionId = url.searchParams.get('sessionId');
+ const transport = sessionId ? sessions.get(sessionId) : undefined;
+ if (transport instanceof SSEServerTransport) {
+ await transport.handlePostMessage(req, res);
+ } else {
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
+ res.end('Session not found');
+ }
+ }
+
+ // ββ Catch-all βββββββββββββββββββββββββββββββββββββββββ
+ else {
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
+ res.end('Not found');
+ }
+ } catch (error) {
+ console.error('[HTTP] Error handling request:', error);
+ if (!res.headersSent) {
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
+ res.end('Internal server error');
+ }
+ }
+ });
+
+ // ββ Heartbeat: detect zombie SSE sessions βββββββββββββββββ
+ const HEARTBEAT_INTERVAL = 30_000;
+ setInterval(() => {
+ for (const [sessionId, sseRes] of sseResponses) {
+ if (sseRes.writableEnded || sseRes.destroyed) {
+ console.log(`[SSE] Stale response detected, removing session: ${sessionId}`);
+ sessions.delete(sessionId);
+ sseResponses.delete(sessionId);
+ continue;
+ }
+ try {
+ sseRes.write(':ping\n\n');
+ } catch {
+ console.log(`[SSE] Heartbeat failed, removing zombie session: ${sessionId}`);
+ sessions.delete(sessionId);
+ sseResponses.delete(sessionId);
+ }
+ }
+ if (sessions.size > 0) {
+ const sseCount = [...sessions.values()].filter(t => t instanceof SSEServerTransport).length;
+ const httpCount = [...sessions.values()].filter(t => t instanceof StreamableHTTPServerTransport).length;
+ console.log(`[SESSIONS] Active: ${sessions.size} (SSE: ${sseCount}, HTTP: ${httpCount})`);
+ }
+ }, HEARTBEAT_INTERVAL);
+
+ httpServer.listen(this.config.port, () => {
+ console.log(`MCP server listening on http://localhost:${this.config.port}`);
+ console.log(` Streamable HTTP: POST|GET|DELETE /mcp`);
+ console.log(` Legacy SSE: GET /sse + POST /message`);
+ });
+ } else {
+ // Default: stdio transport (existing behavior, unchanged)
+ const server = this.createServer();
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+ console.log("MCP server started and listening on stdio.");
+ }
// Start background sync after server is connected
console.log('[SYNC-DEBUG] Initializing background sync...');
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 75930e8b..6caea54a 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,5 +2,7 @@ packages:
- packages/*
- examples/*
+approveBuildsPackages: esbuild tree-sitter tree-sitter-c-sharp tree-sitter-cli tree-sitter-cpp tree-sitter-go tree-sitter-java tree-sitter-javascript tree-sitter-python tree-sitter-rust tree-sitter-scala tree-sitter-typescript unrs-resolver
+
ignoredBuiltDependencies:
- faiss-node