@context-engine-bridge/context-engine-mcp-bridge 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,319 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ # Context-Engine MCP Bridge
3
+
4
+ ## Purpose
5
+
6
+ TypeScript/Node.js MCP bridge server that acts as a proxy between MCP clients (Claude Code, Cursor, Windsurf) and the Context Engine backend. Provides SSE/HTTP transport, OAuth 2.0 authentication, and transparent path mapping between local and container filesystems.
7
+
8
+ ## Key Responsibilities
9
+
10
+ 1. **MCP Protocol Translation**: Converts stdio/HTTP requests from MCP clients to HTTP requests for the Python backend
11
+ 2. **Authentication**: Manages OAuth 2.0 flows and session persistence for multi-user environments
12
+ 3. **Path Mapping**: Translates container paths (`/work/repo/...`) to local workspace paths for client consumption
13
+ 4. **Session Management**: Maintains per-connection session state and default collection/workspace settings
14
+ 5. **Tool Routing**: Routes `memory.*` tools to memory server, other tools to indexer server
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ ┌────────────────────────────────────────────────────────┐
20
+ │ MCP Client (Claude Code, etc.) │
21
+ └──────────────┬─────────────────────────────────────────┘
22
+ │ stdio or HTTP
23
+ ┌──────────────▼─────────────────────────────────────────┐
24
+ │ ctx-mcp-bridge (src/mcpServer.js) │
25
+ │ ┌─────────────────────────────────────────────────┐ │
26
+ │ │ MCP SDK Server (stdio/HTTP transport) │ │
27
+ │ │ - tools/list → merge indexer + memory tools │ │
28
+ │ │ - tools/call → route to appropriate backend │ │
29
+ │ └────────────────┬────────────────────────────────┘ │
30
+ │ │ │
31
+ │ ┌────────────────▼─────────────┐ ┌─────────────────┐ │
32
+ │ │ MCP HTTP Client (indexer) │ │ HTTP Client │ │
33
+ │ │ http://localhost:8003/mcp │ │ (memory) │ │
34
+ │ └──────────────────────────────┘ └─────────────────┘ │
35
+ └────────────────────────────────────────────────────────┘
36
+
37
+ ┌──────────────▼─────────────────────────────────────────┐
38
+ │ Python Backend (indexer_mcp.py, memory_mcp.py) │
39
+ └────────────────────────────────────────────────────────┘
40
+ ```
41
+
42
+ ## Core Files
43
+
44
+ ### src/mcpServer.js (800+ lines)
45
+
46
+ Main MCP server implementation. Key functions:
47
+
48
+ - `createBridgeServer()`: Initializes MCP server with dual backend clients
49
+ - `initializeRemoteClients()`: Connects to indexer and memory servers via StreamableHTTPClientTransport
50
+ - `ListToolsRequestSchema` handler: Merges tools from both backends, deduplicates
51
+ - `CallToolRequestSchema` handler: Routes tools, injects session ID, handles retries
52
+ - `selectClientForTool()`: Routes `memory.*` tools to memory server, others to indexer
53
+ - `fetchBridgeCollectionState()`: Queries `/bridge/state` for active collection overrides
54
+ - Session retry logic: Detects `"No valid session ID"` errors and reinitializes clients
55
+
56
+ **Environment Variables**:
57
+ - `CTXCE_INDEXER_URL`: Default `http://localhost:8003/mcp`
58
+ - `CTXCE_MEMORY_URL`: Default `http://localhost:8002/mcp` (optional)
59
+ - `CTXCE_SESSION_ID`: Explicit session ID (overrides derived session)
60
+ - `CTXCE_TOOL_TIMEOUT_MSEC`: Tool call timeout (default 300000 = 5 min)
61
+ - `CTXCE_TOOL_RETRY_ATTEMPTS`: Retry count for transient errors (default 2)
62
+ - `CTXCE_BRIDGE_STATE_TOKEN`: Auth token for `/bridge/state` endpoint
63
+
64
+ ### src/oauthHandler.js (586 lines)
65
+
66
+ OAuth 2.0 implementation (RFC9728 Protected Resource Metadata + RFC7591 Dynamic Client Registration):
67
+
68
+ - **Endpoints**:
69
+ - `GET /.well-known/oauth-authorization-server`: OAuth metadata
70
+ - `POST /oauth/register`: Dynamic client registration (auto-approve local clients)
71
+ - `GET /oauth/authorize`: Authorization flow (shows login page or auto-approves if session exists)
72
+ - `POST /oauth/store-session`: Stores session from login page, returns auth code
73
+ - `POST /oauth/token`: Exchanges auth code for bearer token
74
+
75
+ - **Security**:
76
+ - PKCE code_challenge validation (TODO: currently skipped for local bridge)
77
+ - Client ID and redirect URI validation against registered clients
78
+ - Origin/Referer header validation (localhost-only for `/oauth/store-session`)
79
+ - 10-minute auth code expiry, 24-hour token expiry
80
+ - Binds to `127.0.0.1` only (no remote access)
81
+
82
+ - **Storage**: In-memory `tokenStore`, `pendingCodes`, `registeredClients` (not persisted across restarts)
83
+
84
+ ### src/resultPathMapping.js (507 lines)
85
+
86
+ Bidirectional path translation between container and client filesystems:
87
+
88
+ **Request Path Mapping** (`maybeRemapToolArgs`):
89
+ - Detects `/work/<repo>/...` container paths in tool args
90
+ - Extracts repo-relative POSIX path (e.g., `/work/myrepo/src/main.py` → `src/main.py`)
91
+ - Handles `path`, `under`, `root`, `subdir`, `path_glob`, `not_glob` parameters
92
+ - Recursively processes nested objects and arrays
93
+
94
+ **Response Path Mapping** (`maybeRemapToolResult`):
95
+ - Parses JSON from `content[].text` or `structuredContent`
96
+ - For each hit object:
97
+ - Computes `rel_path` from container path
98
+ - Joins `rel_path` to workspace root to produce `client_path`
99
+ - Prefers existing `host_path` if it exists and is within workspace
100
+ - Optionally overrides `hit.path` with `client_path` (default: enabled via `CTXCE_BRIDGE_OVERRIDE_PATH=1`)
101
+ - Remaps `related_paths` arrays to absolute local paths
102
+
103
+ **Environment Variables**:
104
+ - `CTXCE_BRIDGE_MAP_PATHS=1`: Enable/disable path mapping (default: enabled)
105
+ - `CTXCE_BRIDGE_OVERRIDE_PATH=1`: Replace `path` field with `client_path` (default: enabled)
106
+ - `CTXCE_BRIDGE_CLIENT_PATH_STRICT=0`: Use `workspace_join` always vs preferring `host_path`
107
+ - `CTXCE_BRIDGE_PATH_DIAGNOSTICS=0`: Add debug fields `client_path_source`, `client_path_joined`
108
+
109
+ ### src/authConfig.js (not shown, ~100 lines)
110
+
111
+ Auth entry persistence to `~/.ctxce/auth.json`:
112
+
113
+ - `loadAuthEntry(backendUrl)`: Load session for specific backend
114
+ - `saveAuthEntry(backendUrl, {sessionId, userId, expiresAt})`: Persist session
115
+ - `deleteAuthEntry(backendUrl)`: Remove session
116
+ - `loadAnyAuthEntry()`: Find any stored session (fallback)
117
+
118
+ ### src/authCli.js (285 lines)
119
+
120
+ CLI commands for auth management:
121
+
122
+ - `ctxce auth login`: Calls `/auth/login` or `/auth/login/password`, saves session
123
+ - `ctxce auth status`: Checks stored session, prints human or JSON output
124
+ - `ctxce auth logout`: Deletes stored session
125
+
126
+ **Fallback logic**: If `--backend-url` omitted, tries stored sessions, then `CTXCE_UPLOAD_ENDPOINT`, then `http://localhost:8004`
127
+
128
+ ### src/cli.js (124 lines)
129
+
130
+ Main CLI dispatcher:
131
+
132
+ - `ctxce mcp-serve`: Starts stdio MCP bridge (default mode for MCP clients)
133
+ - `ctxce mcp-http-serve`: Starts HTTP MCP bridge on port 30810 (used by VS Code extension)
134
+ - `ctxce auth <subcmd>`: Delegates to `authCli.js`
135
+
136
+ **Flags**:
137
+ - `--workspace` / `--path`: Workspace root (default: cwd)
138
+ - `--indexer-url`: Override indexer URL
139
+ - `--memory-url`: Override memory URL (or omit to disable)
140
+ - `--port`: HTTP port (for `mcp-http-serve`)
141
+
142
+ ### bin/ctxce.js (2 lines)
143
+
144
+ Shebang wrapper: `#!/usr/bin/env node`, imports `runCli()` from `src/cli.js`
145
+
146
+ ## Key Features
147
+
148
+ ### 1. Tool Routing
149
+
150
+ ```javascript
151
+ function selectClientForTool(name, indexerClient, memoryClient) {
152
+ const lowered = name.toLowerCase();
153
+ if (memoryClient && lowered.includes("memory")) {
154
+ return memoryClient;
155
+ }
156
+ return indexerClient;
157
+ }
158
+ ```
159
+
160
+ Tools starting with `memory.` or containing "memory" route to memory server; all others route to indexer.
161
+
162
+ ### 2. Session Injection
163
+
164
+ Bridge injects `session` parameter into all tool calls:
165
+ 1. Explicit `CTXCE_SESSION_ID` env var
166
+ 2. Stored auth session from `~/.ctxce/auth.json`
167
+ 3. Fallback: `ctxce-<workspace-hash>` (deterministic per workspace)
168
+
169
+ ### 3. Retry Logic
170
+
171
+ Automatic retry on transient errors (timeouts, network errors, 502/503/504):
172
+ - Default: 2 attempts with 200ms delay
173
+ - Detects session errors (`"No valid session ID"`) and reinitializes clients
174
+ - Configurable via `CTXCE_TOOL_RETRY_ATTEMPTS` and `CTXCE_TOOL_RETRY_DELAY_MSEC`
175
+
176
+ ### 4. Collection State Override
177
+
178
+ Queries `/bridge/state?collection=...&repo_name=...` on startup to resolve active collection:
179
+ - Prefers `serving_collection` from backend over `ctx_config.json`
180
+ - Falls back to `default_collection` from `ctx_config.json`
181
+ - Logs active collection for transparency
182
+
183
+ ### 5. Path Mapping Examples
184
+
185
+ **Request**: Client sends `path: "/Users/dev/myrepo/src/main.py"`
186
+ → Bridge normalizes to `src/main.py` (repo-relative)
187
+ → Backend receives `path: "src/main.py"`
188
+
189
+ **Response**: Backend returns `path: "/work/myrepo/src/main.py"`, `container_path: "/work/myrepo/src/main.py"`
190
+ → Bridge computes `rel_path: "src/main.py"`, `client_path: "/Users/dev/myrepo/src/main.py"`
191
+ → Client receives `path: "/Users/dev/myrepo/src/main.py"` (if `CTXCE_BRIDGE_OVERRIDE_PATH=1`)
192
+
193
+ ## Usage Patterns
194
+
195
+ ### Stdio Mode (for MCP clients)
196
+
197
+ ```bash
198
+ ctxce mcp-serve --workspace /path/to/repo
199
+ ```
200
+
201
+ Add to Claude Desktop `claude_desktop_config.json`:
202
+ ```json
203
+ {
204
+ "mcpServers": {
205
+ "context-engine": {
206
+ "command": "ctxce",
207
+ "args": ["mcp-serve", "--workspace", "/path/to/repo"]
208
+ }
209
+ }
210
+ }
211
+ ```
212
+
213
+ ### HTTP Mode (for VS Code extension)
214
+
215
+ ```bash
216
+ ctxce mcp-http-serve --workspace /path/to/repo --port 30810
217
+ ```
218
+
219
+ Extension uses HTTP transport to avoid stdio buffering issues.
220
+
221
+ ### Authentication Flow
222
+
223
+ 1. User initiates OAuth via browser: `GET /oauth/authorize?client_id=...&redirect_uri=...&code_challenge=...`
224
+ 2. Bridge checks for existing session in `~/.ctxce/auth.json`
225
+ 3. If no session, shows login page with backend URL input
226
+ 4. User submits credentials, page POSTs to backend `/auth/login`
227
+ 5. Backend returns `session_id`, page POSTs to `/oauth/store-session`
228
+ 6. Bridge saves session to `~/.ctxce/auth.json`, generates auth code, redirects with code
229
+ 7. Client exchanges code for bearer token via `/oauth/token`
230
+ 8. Client includes `Authorization: Bearer <token>` in subsequent MCP requests
231
+
232
+ ## Testing
233
+
234
+ No automated tests currently. Manual testing via:
235
+
236
+ 1. Start backend: `docker-compose up -d`
237
+ 2. Run bridge: `ctxce mcp-http-serve --workspace /path/to/repo`
238
+ 3. Test OAuth flow: Open `http://127.0.0.1:30810/oauth/authorize?client_id=test&redirect_uri=http://localhost/callback&code_challenge=...`
239
+ 4. Test MCP tools: Use MCP client (Claude Code) or `curl` to POST tool calls to `/mcp`
240
+
241
+ ## Debugging
242
+
243
+ Set `CTXCE_DEBUG_LOG=/tmp/ctxce.log` to log all bridge activity:
244
+ ```bash
245
+ export CTXCE_DEBUG_LOG=/tmp/ctxce.log
246
+ ctxce mcp-serve --workspace /path/to/repo
247
+ tail -f /tmp/ctxce.log
248
+ ```
249
+
250
+ Common issues:
251
+ - **Session errors**: Check `~/.ctxce/auth.json` has valid `sessionId`
252
+ - **Path mapping issues**: Set `CTXCE_BRIDGE_PATH_DIAGNOSTICS=1` to see `client_path_source`
253
+ - **Tool routing issues**: Check tool name includes `"memory"` for memory server routing
254
+ - **Timeout errors**: Increase `CTXCE_TOOL_TIMEOUT_MSEC` for slow queries
255
+
256
+ ## Integration Points
257
+
258
+ ### Upstream (MCP Clients)
259
+ - **Claude Code**: Uses stdio transport via `claude_desktop_config.json`
260
+ - **Cursor**: Supports MCP via settings
261
+ - **VS Code Extension**: Uses HTTP transport on port 30810
262
+
263
+ ### Downstream (Python Backend)
264
+ - **indexer_mcp.py**: Provides 30+ search/graph tools on port 8003
265
+ - **memory_mcp.py**: Provides memory storage tools on port 8002
266
+ - **upload_service.py**: Auth backend on port 8004 (`/auth/login`, `/bridge/state`)
267
+
268
+ ## Security Considerations
269
+
270
+ 1. **Local-only binding**: HTTP server binds to `127.0.0.1` only (no remote access)
271
+ 2. **Origin validation**: `/oauth/store-session` requires `Origin` or `Referer` header from localhost
272
+ 3. **Client validation**: OAuth endpoints validate `client_id` and `redirect_uri` against registered clients
273
+ 4. **Session persistence**: `~/.ctxce/auth.json` stores sessions (file permissions: 0600)
274
+ 5. **Token expiry**: Auth codes expire in 10 min, bearer tokens in 24 hours
275
+ 6. **PKCE**: Code challenge method `S256` supported (validation TODO)
276
+
277
+ ## Configuration Files
278
+
279
+ ### ~/.ctxce/auth.json
280
+
281
+ ```json
282
+ {
283
+ "http://localhost:8004": {
284
+ "sessionId": "sess_abc123",
285
+ "userId": "user-456",
286
+ "expiresAt": 1704974400
287
+ }
288
+ }
289
+ ```
290
+
291
+ ### ctx_config.json (in workspace)
292
+
293
+ ```json
294
+ {
295
+ "default_collection": "myrepo-abc123",
296
+ "default_mode": "refrag",
297
+ "default_under": "src/",
298
+ "repo_name": "myrepo"
299
+ }
300
+ ```
301
+
302
+ Bridge reads this on startup and sends to backend via `set_session_defaults`.
303
+
304
+ ## Future Enhancements
305
+
306
+ 1. **PKCE validation**: Implement `code_verifier` hash validation in `/oauth/token`
307
+ 2. **Persistent storage**: Migrate OAuth state to Redis/SQLite (currently in-memory)
308
+ 3. **Token refresh**: Support `refresh_token` grant type for long-lived sessions
309
+ 4. **Multi-workspace**: Allow clients to switch workspaces without restarting bridge
310
+ 5. **Rate limiting**: Add per-client rate limits for tool calls
311
+ 6. **Metrics**: Expose Prometheus metrics for tool call latency, error rates
312
+
313
+ ## Related Documentation
314
+
315
+ - [MCP SDK Documentation](https://modelcontextprotocol.io/docs)
316
+ - [OAuth 2.0 RFC9728](https://www.rfc-editor.org/rfc/rfc9728.html) (Protected Resource Metadata)
317
+ - [OAuth 2.0 RFC7591](https://www.rfc-editor.org/rfc/rfc7591.html) (Dynamic Client Registration)
318
+ - [Context Engine Python Backend](../scripts/AGENTS.md)
319
+ - [VS Code Extension Integration](../README.md#vs-code-extension)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
5
5
  "bin": {
6
6
  "ctxce": "bin/ctxce.js",
@@ -258,10 +258,16 @@ function remapHitPaths(hit, workspaceRoot) {
258
258
  if (!containerPath && rawPath) {
259
259
  containerPath = rawPath;
260
260
  }
261
- const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
262
261
  const out = { ...hit };
263
- if (relPath) {
264
- out.rel_path = relPath;
262
+ // Respect server's rel_path if already provided and non-empty; only compute if missing
263
+ const serverRelPath = typeof hit.rel_path === "string" ? hit.rel_path.trim() : "";
264
+ if (serverRelPath) {
265
+ out.rel_path = serverRelPath;
266
+ } else {
267
+ const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
268
+ if (relPath) {
269
+ out.rel_path = relPath;
270
+ }
265
271
  }
266
272
  // Remap related_paths nested under each hit (repo_search/hybrid_search emit this per result).
267
273
  try {
@@ -271,9 +277,10 @@ function remapHitPaths(hit, workspaceRoot) {
271
277
  } catch {
272
278
  // ignore
273
279
  }
274
- if (workspaceRoot && relPath) {
280
+ const finalRelPath = out.rel_path || "";
281
+ if (workspaceRoot && finalRelPath) {
275
282
  try {
276
- const relNative = _posixToNative(relPath);
283
+ const relNative = _posixToNative(finalRelPath);
277
284
  const candidate = path.join(workspaceRoot, relNative);
278
285
  const diagnostics = envTruthy(process.env.CTXCE_BRIDGE_PATH_DIAGNOSTICS, false);
279
286
  const strictClientPath = envTruthy(process.env.CTXCE_BRIDGE_CLIENT_PATH_STRICT, false);
@@ -315,8 +322,8 @@ function remapHitPaths(hit, workspaceRoot) {
315
322
  if (overridePath) {
316
323
  if (typeof out.client_path === "string" && out.client_path) {
317
324
  out.path = out.client_path;
318
- } else if (relPath) {
319
- out.path = relPath;
325
+ } else if (finalRelPath) {
326
+ out.path = finalRelPath;
320
327
  }
321
328
  }
322
329
  return out;