@context-engine-bridge/context-engine-mcp-bridge 0.0.4 → 0.0.6

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/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Context Engine MCP Bridge
2
+
3
+ `@context-engine-bridge/context-engine-mcp-bridge` provides the `ctxce` CLI, a
4
+ Model Context Protocol (MCP) bridge that speaks to the Context Engine indexer
5
+ and memory servers and exposes them as a single MCP server.
6
+
7
+ It is primarily used by the VS Code **Context Engine Uploader** extension,
8
+ available on the Marketplace:
9
+
10
+ - <https://marketplace.visualstudio.com/items?itemName=context-engine.context-engine-uploader>
11
+
12
+ The bridge can also be run standalone (e.g. from a terminal, or wired into
13
+ other MCP clients) as long as the Context Engine stack is running.
14
+
15
+ ## Prerequisites
16
+
17
+ - Node.js **>= 18** (see `engines` in `package.json`).
18
+ - A running Context Engine stack (e.g. via `docker-compose.dev-remote.yml`) with:
19
+ - MCP indexer HTTP endpoint (default: `http://localhost:8003/mcp`).
20
+ - MCP memory HTTP endpoint (optional, default: `http://localhost:8002/mcp`).
21
+ - For optional auth:
22
+ - The upload/auth services must be configured with `CTXCE_AUTH_ENABLED=1` and
23
+ a reachable auth backend URL (e.g. `http://localhost:8004`).
24
+
25
+ ## Installation
26
+
27
+ You can install the package globally, or run it via `npx`.
28
+
29
+ ### Global install
30
+
31
+ ```bash
32
+ npm install -g @context-engine-bridge/context-engine-mcp-bridge
33
+ ```
34
+
35
+ This installs the `ctxce` (and `ctxce-bridge`) CLI in your PATH.
36
+
37
+ ### Using npx (no global install)
38
+
39
+ ```bash
40
+ npx @context-engine-bridge/context-engine-mcp-bridge ctxce --help
41
+ ```
42
+
43
+ The examples below assume `ctxce` is available on your PATH; if you use `npx`,
44
+ just prefix commands with `npx @context-engine-bridge/context-engine-mcp-bridge`.
45
+
46
+ ## CLI overview
47
+
48
+ The main entrypoint is:
49
+
50
+ ```bash
51
+ ctxce <command> [...args]
52
+ ```
53
+
54
+ Supported commands (from `src/cli.js`):
55
+
56
+ - `ctxce mcp-serve` – stdio MCP bridge (for stdio-based MCP clients).
57
+ - `ctxce mcp-http-serve` – HTTP MCP bridge (for HTTP-based MCP clients).
58
+ - `ctxce auth <subcmd>` – auth helper commands (`login`, `status`, `logout`).
59
+
60
+ ### Environment variables
61
+
62
+ These environment variables are respected by the bridge:
63
+
64
+ - `CTXCE_INDEXER_URL` – MCP indexer URL (default: `http://localhost:8003/mcp`).
65
+ - `CTXCE_MEMORY_URL` – MCP memory URL, or empty/omitted to disable memory
66
+ (default: `http://localhost:8002/mcp`).
67
+ - `CTXCE_HTTP_PORT` – port for `mcp-http-serve` (default: `30810`).
68
+
69
+ For auth (optional, shared with the upload/auth backend):
70
+
71
+ - `CTXCE_AUTH_ENABLED` – whether auth is enabled in the backend.
72
+ - `CTXCE_AUTH_BACKEND_URL` – auth backend URL (e.g. `http://localhost:8004`).
73
+ - `CTXCE_AUTH_TOKEN` – dev/shared token for `ctxce auth login`.
74
+ - `CTXCE_AUTH_SESSION_TTL_SECONDS` – session TTL / sliding expiry (seconds).
75
+
76
+ The CLI also stores auth sessions in `~/.ctxce/auth.json`, keyed by backend URL.
77
+
78
+ ## Running the MCP bridge (stdio)
79
+
80
+ The stdio bridge is suitable for MCP clients that speak stdio directly (for
81
+ example, certain editors or tools that expect an MCP server on stdin/stdout).
82
+
83
+ ```bash
84
+ ctxce mcp-serve \
85
+ --workspace /path/to/your/workspace \
86
+ --indexer-url http://localhost:8003/mcp \
87
+ --memory-url http://localhost:8002/mcp
88
+ ```
89
+
90
+ Flags:
91
+
92
+ - `--workspace` / `--path` – workspace root (default: current working directory).
93
+ - `--indexer-url` – override indexer URL (default: `CTXCE_INDEXER_URL` or
94
+ `http://localhost:8003/mcp`).
95
+ - `--memory-url` – override memory URL (default: `CTXCE_MEMORY_URL` or
96
+ disabled when empty).
97
+
98
+ ## Running the MCP bridge (HTTP)
99
+
100
+ The HTTP bridge exposes the MCP server via an HTTP endpoint (default
101
+ `http://127.0.0.1:30810/mcp`) and is what the VS Code extension uses in its
102
+ `http` transport mode.
103
+
104
+ ```bash
105
+ ctxce mcp-http-serve \
106
+ --workspace /path/to/your/workspace \
107
+ --indexer-url http://localhost:8003/mcp \
108
+ --memory-url http://localhost:8002/mcp \
109
+ --port 30810
110
+ ```
111
+
112
+ Flags:
113
+
114
+ - `--workspace` / `--path` – workspace root (default: current working directory).
115
+ - `--indexer-url` – MCP indexer URL.
116
+ - `--memory-url` – MCP memory URL (or omit/empty to disable memory).
117
+ - `--port` – HTTP port for the bridge (default: `CTXCE_HTTP_PORT`
118
+ or `30810`).
119
+
120
+ Once running, you can point an MCP client at:
121
+
122
+ ```text
123
+ http://127.0.0.1:<port>/mcp
124
+ ```
125
+
126
+ ## Auth helper commands (`ctxce auth ...`)
127
+
128
+ These commands are used both by the VS Code extension and standalone flows to
129
+ log in and manage auth sessions for the backend.
130
+
131
+ ### Login (token)
132
+
133
+ ```bash
134
+ ctxce auth login \
135
+ --backend-url http://localhost:8004 \
136
+ --token $CTXCE_AUTH_SHARED_TOKEN
137
+ ```
138
+
139
+ This hits the backend `/auth/login` endpoint and stores a session entry in
140
+ `~/.ctxce/auth.json` under the given backend URL.
141
+
142
+ ### Login (username/password)
143
+
144
+ ```bash
145
+ ctxce auth login \
146
+ --backend-url http://localhost:8004 \
147
+ --username your-user \
148
+ --password your-password
149
+ ```
150
+
151
+ This calls `/auth/login/password` and persists the returned session the same
152
+ way as the token flow.
153
+
154
+ ### Status
155
+
156
+ Human-readable status:
157
+
158
+ ```bash
159
+ ctxce auth status --backend-url http://localhost:8004
160
+ ```
161
+
162
+ Machine-readable status (used by the VS Code extension):
163
+
164
+ ```bash
165
+ ctxce auth status --backend-url http://localhost:8004 --json
166
+ ```
167
+
168
+ The `--json` variant prints a single JSON object to stdout, for example:
169
+
170
+ ```json
171
+ {
172
+ "backendUrl": "http://localhost:8004",
173
+ "state": "ok", // "ok" | "missing" | "expired" | "missing_backend"
174
+ "sessionId": "...",
175
+ "userId": "user-123",
176
+ "expiresAt": 0 // 0 or a Unix timestamp
177
+ }
178
+ ```
179
+
180
+ Exit codes:
181
+
182
+ - `0` – `state: "ok"` (valid session present).
183
+ - `1` – `state: "missing"` or `"missing_backend"`.
184
+ - `2` – `state: "expired"`.
185
+
186
+ ### Logout
187
+
188
+ ```bash
189
+ ctxce auth logout --backend-url http://localhost:8004
190
+ ```
191
+
192
+ Removes the stored auth entry for the given backend URL from
193
+ `~/.ctxce/auth.json`.
194
+
195
+ ## Relationship to the VS Code extension
196
+
197
+ The VS Code **Context Engine Uploader** extension is the recommended way to use
198
+ this bridge for day-to-day development. It:
199
+
200
+ - Launches the standalone upload client to push code into the remote stack.
201
+ - Starts/stops the MCP HTTP bridge (`ctxce mcp-http-serve`) for the active
202
+ workspace when `autoStartMcpBridge` is enabled.
203
+ - Uses `ctxce auth status --json` and `ctxce auth login` under the hood to
204
+ manage user sessions via UI prompts.
205
+
206
+ This package README is aimed at advanced users who want to:
207
+
208
+ - Run the MCP bridge outside of VS Code.
209
+ - Integrate the Context Engine MCP servers with other MCP-compatible clients.
210
+
211
+ You can safely mix both approaches: the extension and the standalone bridge
212
+ share the same auth/session storage in `~/.ctxce/auth.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
5
5
  "bin": {
6
6
  "ctxce": "bin/ctxce.js",
package/src/authCli.js ADDED
@@ -0,0 +1,206 @@
1
+ import process from "node:process";
2
+ import { loadAuthEntry, saveAuthEntry, deleteAuthEntry } from "./authConfig.js";
3
+
4
+ function parseAuthArgs(args) {
5
+ let backendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
6
+ let token = process.env.CTXCE_AUTH_TOKEN || "";
7
+ let username = process.env.CTXCE_AUTH_USERNAME || "";
8
+ let password = process.env.CTXCE_AUTH_PASSWORD || "";
9
+ let outputJson = false;
10
+ for (let i = 0; i < args.length; i += 1) {
11
+ const a = args[i];
12
+ if ((a === "--backend-url" || a === "--auth-url") && i + 1 < args.length) {
13
+ backendUrl = args[i + 1];
14
+ i += 1;
15
+ continue;
16
+ }
17
+ if ((a === "--token" || a === "--api-key") && i + 1 < args.length) {
18
+ token = args[i + 1];
19
+ i += 1;
20
+ continue;
21
+ }
22
+ if ((a === "--username" || a === "--user") && i + 1 < args.length) {
23
+ username = args[i + 1];
24
+ i += 1;
25
+ continue;
26
+ }
27
+ if ((a === "--password" || a === "--pass") && i + 1 < args.length) {
28
+ password = args[i + 1];
29
+ i += 1;
30
+ continue;
31
+ }
32
+ if (a === "--json" || a === "-j") {
33
+ outputJson = true;
34
+ continue;
35
+ }
36
+ }
37
+ return { backendUrl, token, username, password, outputJson };
38
+ }
39
+
40
+ function getBackendUrl(backendUrl) {
41
+ return (backendUrl || process.env.CTXCE_AUTH_BACKEND_URL || "").trim();
42
+ }
43
+
44
+ function requireBackendUrl(backendUrl) {
45
+ const url = getBackendUrl(backendUrl);
46
+ if (!url) {
47
+ console.error("[ctxce] Auth backend URL not configured. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
48
+ process.exit(1);
49
+ }
50
+ return url;
51
+ }
52
+
53
+ function outputJsonStatus(url, state, entry, rawExpires) {
54
+ const expiresAt = typeof rawExpires === "number"
55
+ ? rawExpires
56
+ : entry && typeof entry.expiresAt === "number"
57
+ ? entry.expiresAt
58
+ : null;
59
+ console.log(JSON.stringify({
60
+ backendUrl: url,
61
+ state,
62
+ sessionId: entry && entry.sessionId ? entry.sessionId : null,
63
+ userId: entry && entry.userId ? entry.userId : null,
64
+ expiresAt,
65
+ }));
66
+ }
67
+
68
+ async function doLogin(args) {
69
+ const { backendUrl, token, username, password } = parseAuthArgs(args);
70
+ const url = requireBackendUrl(backendUrl);
71
+ const trimmedUser = (username || "").trim();
72
+ const usePassword = trimmedUser && (password || "").length > 0;
73
+
74
+ let body;
75
+ let target;
76
+ if (usePassword) {
77
+ body = {
78
+ username: trimmedUser,
79
+ password,
80
+ workspace: process.cwd(),
81
+ };
82
+ target = url.replace(/\/+$/, "") + "/auth/login/password";
83
+ } else {
84
+ body = {
85
+ client: "ctxce",
86
+ workspace: process.cwd(),
87
+ };
88
+ if (token) {
89
+ body.token = token;
90
+ }
91
+ target = url.replace(/\/+$/, "") + "/auth/login";
92
+ }
93
+ let resp;
94
+ try {
95
+ resp = await fetch(target, {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify(body),
99
+ });
100
+ } catch (err) {
101
+ console.error("[ctxce] Auth login request failed:", String(err));
102
+ process.exit(1);
103
+ }
104
+ if (!resp || !resp.ok) {
105
+ console.error("[ctxce] Auth login failed with status", resp ? resp.status : "<no-response>");
106
+ process.exit(1);
107
+ }
108
+ let data;
109
+ try {
110
+ data = await resp.json();
111
+ } catch (err) {
112
+ data = {};
113
+ }
114
+ const sessionId = data.session_id || data.sessionId || null;
115
+ const userId = data.user_id || data.userId || null;
116
+ const expiresAt = data.expires_at || data.expiresAt || null;
117
+ if (!sessionId) {
118
+ console.error("[ctxce] Auth login response missing session id.");
119
+ process.exit(1);
120
+ }
121
+ saveAuthEntry(url, { sessionId, userId, expiresAt });
122
+ console.error("[ctxce] Auth login successful for", url);
123
+ }
124
+
125
+ async function doStatus(args) {
126
+ const { backendUrl, outputJson } = parseAuthArgs(args);
127
+ const url = getBackendUrl(backendUrl);
128
+ if (!url) {
129
+ if (outputJson) {
130
+ outputJsonStatus("", "missing_backend", null, null);
131
+ process.exit(1);
132
+ }
133
+ console.error("[ctxce] Auth backend URL not configured. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
134
+ process.exit(1);
135
+ }
136
+ let entry;
137
+ try {
138
+ entry = loadAuthEntry(url);
139
+ } catch (err) {
140
+ entry = null;
141
+ }
142
+ const nowSecs = Math.floor(Date.now() / 1000);
143
+ const rawExpires = entry && typeof entry.expiresAt === "number" ? entry.expiresAt : null;
144
+ const hasSession = !!(entry && typeof entry.sessionId === "string" && entry.sessionId);
145
+ const expired = !!(rawExpires && rawExpires > 0 && rawExpires < nowSecs);
146
+
147
+ if (!entry || !hasSession) {
148
+ if (outputJson) {
149
+ outputJsonStatus(url, "missing", null, rawExpires);
150
+ process.exit(1);
151
+ }
152
+ console.error("[ctxce] Not logged in for", url);
153
+ process.exit(1);
154
+ }
155
+
156
+ if (expired) {
157
+ if (outputJson) {
158
+ outputJsonStatus(url, "expired", entry, rawExpires);
159
+ process.exit(2);
160
+ }
161
+ console.error("[ctxce] Stored auth session appears expired for", url);
162
+ if (rawExpires) {
163
+ console.error("[ctxce] Session expired at", rawExpires);
164
+ }
165
+ process.exit(2);
166
+ }
167
+
168
+ if (outputJson) {
169
+ outputJsonStatus(url, "ok", entry, rawExpires);
170
+ return;
171
+ }
172
+ console.error("[ctxce] Logged in to", url, "as", entry.userId || "<unknown>");
173
+ if (rawExpires) {
174
+ console.error("[ctxce] Session expires at", rawExpires);
175
+ }
176
+ }
177
+
178
+ async function doLogout(args) {
179
+ const { backendUrl } = parseAuthArgs(args);
180
+ const url = requireBackendUrl(backendUrl);
181
+ const entry = loadAuthEntry(url);
182
+ if (!entry) {
183
+ console.error("[ctxce] No stored auth session for", url);
184
+ return;
185
+ }
186
+ deleteAuthEntry(url);
187
+ console.error("[ctxce] Logged out from", url);
188
+ }
189
+
190
+ export async function runAuthCommand(subcommand, args) {
191
+ const sub = (subcommand || "").toLowerCase();
192
+ if (sub === "login") {
193
+ await doLogin(args || []);
194
+ return;
195
+ }
196
+ if (sub === "status") {
197
+ await doStatus(args || []);
198
+ return;
199
+ }
200
+ if (sub === "logout") {
201
+ await doLogout(args || []);
202
+ return;
203
+ }
204
+ console.error("Usage: ctxce auth <login|status|logout> [--backend-url <url>] [--token <token>]");
205
+ process.exit(1);
206
+ }
@@ -0,0 +1,84 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const CONFIG_DIR_NAME = ".ctxce";
6
+ const CONFIG_BASENAME = "auth.json";
7
+
8
+ function getConfigPath() {
9
+ const home = os.homedir() || process.cwd();
10
+ const dir = path.join(home, CONFIG_DIR_NAME);
11
+ return path.join(dir, CONFIG_BASENAME);
12
+ }
13
+
14
+ function readConfig() {
15
+ try {
16
+ const cfgPath = getConfigPath();
17
+ const raw = fs.readFileSync(cfgPath, "utf8");
18
+ const parsed = JSON.parse(raw);
19
+ if (parsed && typeof parsed === "object") {
20
+ return parsed;
21
+ }
22
+ } catch (err) {
23
+ }
24
+ return {};
25
+ }
26
+
27
+ function writeConfig(data) {
28
+ try {
29
+ const cfgPath = getConfigPath();
30
+ const dir = path.dirname(cfgPath);
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+ fs.writeFileSync(cfgPath, JSON.stringify(data, null, 2), "utf8");
35
+ } catch (err) {
36
+ }
37
+ }
38
+
39
+ export function loadAuthEntry(backendUrl) {
40
+ if (!backendUrl) {
41
+ return null;
42
+ }
43
+ const all = readConfig();
44
+ const key = String(backendUrl);
45
+ const entry = all[key];
46
+ if (!entry || typeof entry !== "object") {
47
+ return null;
48
+ }
49
+ return entry;
50
+ }
51
+
52
+ export function saveAuthEntry(backendUrl, entry) {
53
+ if (!backendUrl || !entry || typeof entry !== "object") {
54
+ return;
55
+ }
56
+ const all = readConfig();
57
+ const key = String(backendUrl);
58
+ all[key] = entry;
59
+ writeConfig(all);
60
+ }
61
+
62
+ export function deleteAuthEntry(backendUrl) {
63
+ if (!backendUrl) {
64
+ return;
65
+ }
66
+ const all = readConfig();
67
+ const key = String(backendUrl);
68
+ if (Object.prototype.hasOwnProperty.call(all, key)) {
69
+ delete all[key];
70
+ writeConfig(all);
71
+ }
72
+ }
73
+
74
+ export function loadAnyAuthEntry() {
75
+ const all = readConfig();
76
+ const keys = Object.keys(all);
77
+ for (const key of keys) {
78
+ const entry = all[key];
79
+ if (entry && typeof entry === "object") {
80
+ return { backendUrl: key, entry };
81
+ }
82
+ }
83
+ return null;
84
+ }
package/src/cli.js CHANGED
@@ -4,11 +4,19 @@ import process from "node:process";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { runMcpServer, runHttpMcpServer } from "./mcpServer.js";
7
+ import { runAuthCommand } from "./authCli.js";
7
8
 
8
9
  export async function runCli() {
9
10
  const argv = process.argv.slice(2);
10
11
  const cmd = argv[0];
11
12
 
13
+ if (cmd === "auth") {
14
+ const sub = argv[1] || "";
15
+ const args = argv.slice(2);
16
+ await runAuthCommand(sub, args);
17
+ return;
18
+ }
19
+
12
20
  if (cmd === "mcp-http-serve") {
13
21
  const args = argv.slice(1);
14
22
  let workspace = process.cwd();
@@ -109,7 +117,7 @@ export async function runCli() {
109
117
 
110
118
  // eslint-disable-next-line no-console
111
119
  console.error(
112
- `Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] | ${binName} mcp-http-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] [--port <port>]`,
120
+ `Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] | ${binName} mcp-http-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] [--port <port>] | ${binName} auth <login|status|logout> [--backend-url <url>] [--token <token>] [--username <name> --password <pass>]`,
113
121
  );
114
122
  process.exit(1);
115
123
  }
package/src/mcpServer.js CHANGED
@@ -120,6 +120,118 @@ function selectClientForTool(name, indexerClient, memoryClient) {
120
120
  }
121
121
  return indexerClient;
122
122
  }
123
+
124
+ function isSessionError(error) {
125
+ try {
126
+ const msg =
127
+ (error && typeof error.message === "string" && error.message) ||
128
+ (typeof error === "string" ? error : String(error || ""));
129
+ if (!msg) {
130
+ return false;
131
+ }
132
+ return (
133
+ msg.includes("No valid session ID") ||
134
+ msg.includes("Mcp-Session-Id header is required") ||
135
+ msg.includes("Server not initialized") ||
136
+ msg.includes("Session not found")
137
+ );
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ function getBridgeRetryAttempts() {
144
+ try {
145
+ const raw = process.env.CTXCE_TOOL_RETRY_ATTEMPTS;
146
+ if (!raw) {
147
+ return 2;
148
+ }
149
+ const parsed = Number.parseInt(String(raw), 10);
150
+ if (!Number.isFinite(parsed) || parsed <= 0) {
151
+ return 1;
152
+ }
153
+ return parsed;
154
+ } catch {
155
+ return 2;
156
+ }
157
+ }
158
+
159
+ function getBridgeRetryDelayMs() {
160
+ try {
161
+ const raw = process.env.CTXCE_TOOL_RETRY_DELAY_MSEC;
162
+ if (!raw) {
163
+ return 200;
164
+ }
165
+ const parsed = Number.parseInt(String(raw), 10);
166
+ if (!Number.isFinite(parsed) || parsed < 0) {
167
+ return 0;
168
+ }
169
+ return parsed;
170
+ } catch {
171
+ return 200;
172
+ }
173
+ }
174
+
175
+ function isTransientToolError(error) {
176
+ try {
177
+ const msg =
178
+ (error && typeof error.message === "string" && error.message) ||
179
+ (typeof error === "string" ? error : String(error || ""));
180
+ if (!msg) {
181
+ return false;
182
+ }
183
+ const lower = msg.toLowerCase();
184
+
185
+ if (
186
+ lower.includes("timed out") ||
187
+ lower.includes("timeout") ||
188
+ lower.includes("time-out")
189
+ ) {
190
+ return true;
191
+ }
192
+
193
+ if (
194
+ lower.includes("econnreset") ||
195
+ lower.includes("econnrefused") ||
196
+ lower.includes("etimedout") ||
197
+ lower.includes("enotfound") ||
198
+ lower.includes("ehostunreach") ||
199
+ lower.includes("enetunreach")
200
+ ) {
201
+ return true;
202
+ }
203
+
204
+ if (
205
+ lower.includes("bad gateway") ||
206
+ lower.includes("gateway timeout") ||
207
+ lower.includes("service unavailable") ||
208
+ lower.includes(" 502 ") ||
209
+ lower.includes(" 503 ") ||
210
+ lower.includes(" 504 ")
211
+ ) {
212
+ return true;
213
+ }
214
+
215
+ if (lower.includes("network error")) {
216
+ return true;
217
+ }
218
+
219
+ if (typeof error.code === "number" && error.code === -32001 && !isSessionError(error)) {
220
+ return true;
221
+ }
222
+ if (
223
+ typeof error.code === "string" &&
224
+ error.code.toLowerCase &&
225
+ error.code.toLowerCase().includes("timeout")
226
+ ) {
227
+ return true;
228
+ }
229
+
230
+ return false;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
123
235
  // MCP stdio server implemented using the official MCP TypeScript SDK.
124
236
  // Acts as a low-level proxy for tools, forwarding tools/list and tools/call
125
237
  // to the remote qdrant-indexer MCP server while adding a local `ping` tool.
@@ -135,6 +247,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
135
247
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
136
248
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
137
249
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
250
+ import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
138
251
 
139
252
  async function createBridgeServer(options) {
140
253
  const workspace = options.workspace || process.cwd();
@@ -162,59 +275,61 @@ async function createBridgeServer(options) {
162
275
  );
163
276
  }
164
277
 
165
- // High-level MCP client for the remote HTTP /mcp indexer
166
- const indexerTransport = new StreamableHTTPClientTransport(indexerUrl);
167
- const indexerClient = new Client(
168
- {
169
- name: "ctx-context-engine-bridge-http-client",
170
- version: "0.0.1",
171
- },
172
- {
173
- capabilities: {
174
- tools: {},
175
- resources: {},
176
- prompts: {},
177
- },
178
- },
179
- );
180
-
181
- try {
182
- await indexerClient.connect(indexerTransport);
183
- } catch (err) {
184
- debugLog("[ctxce] Failed to connect MCP HTTP client to indexer: " + String(err));
185
- }
186
-
278
+ let indexerClient = null;
187
279
  let memoryClient = null;
188
- if (memoryUrl) {
189
- try {
190
- const memoryTransport = new StreamableHTTPClientTransport(memoryUrl);
191
- memoryClient = new Client(
192
- {
193
- name: "ctx-context-engine-bridge-memory-client",
194
- version: "0.0.1",
195
- },
196
- {
197
- capabilities: {
198
- tools: {},
199
- resources: {},
200
- prompts: {},
201
- },
202
- },
203
- );
204
- await memoryClient.connect(memoryTransport);
205
- debugLog(`[ctxce] Connected memory MCP client: ${memoryUrl}`);
206
- } catch (err) {
207
- debugLog("[ctxce] Failed to connect memory MCP client: " + String(err));
208
- memoryClient = null;
209
- }
210
- }
211
280
 
212
281
  // Derive a simple session identifier for this bridge process. In the
213
282
  // future this can be made user-aware (e.g. from auth), but for now we
214
283
  // keep it deterministic per workspace to help the indexer reuse
215
284
  // session-scoped defaults.
216
- const sessionId =
217
- process.env.CTXCE_SESSION_ID || `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
285
+ const explicitSession = process.env.CTXCE_SESSION_ID || "";
286
+ const authBackendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
287
+ let sessionId = explicitSession;
288
+
289
+ if (!sessionId) {
290
+ let backendToUse = authBackendUrl;
291
+ let entry = null;
292
+
293
+ if (backendToUse) {
294
+ try {
295
+ entry = loadAuthEntry(backendToUse);
296
+ } catch (err) {
297
+ entry = null;
298
+ }
299
+ }
300
+
301
+ if (!entry) {
302
+ try {
303
+ const any = loadAnyAuthEntry();
304
+ if (any && any.entry) {
305
+ backendToUse = any.backendUrl;
306
+ entry = any.entry;
307
+ }
308
+ } catch (err) {
309
+ entry = null;
310
+ }
311
+ }
312
+
313
+ if (entry) {
314
+ let expired = false;
315
+ const rawExpires = entry.expiresAt;
316
+ if (typeof rawExpires === "number" && Number.isFinite(rawExpires) && rawExpires > 0) {
317
+ const nowSecs = Math.floor(Date.now() / 1000);
318
+ if (rawExpires < nowSecs) {
319
+ expired = true;
320
+ }
321
+ }
322
+ if (!expired && typeof entry.sessionId === "string" && entry.sessionId) {
323
+ sessionId = entry.sessionId;
324
+ } else if (expired) {
325
+ debugLog("[ctxce] Stored auth session appears expired; please run `ctxce auth login` again.");
326
+ }
327
+ }
328
+ }
329
+
330
+ if (!sessionId) {
331
+ sessionId = `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
332
+ }
218
333
 
219
334
  // Best-effort: inform the indexer of default collection and session.
220
335
  // If this fails we still proceed, falling back to per-call injection.
@@ -229,13 +344,81 @@ async function createBridgeServer(options) {
229
344
  defaultsPayload.under = defaultUnder;
230
345
  }
231
346
 
232
- if (Object.keys(defaultsPayload).length > 1) {
233
- await sendSessionDefaults(indexerClient, defaultsPayload, "indexer");
234
- if (memoryClient) {
235
- await sendSessionDefaults(memoryClient, defaultsPayload, "memory");
347
+ async function initializeRemoteClients(forceRecreate = false) {
348
+ if (!forceRecreate && indexerClient) {
349
+ return;
350
+ }
351
+
352
+ if (forceRecreate) {
353
+ try {
354
+ debugLog("[ctxce] Reinitializing remote MCP clients after session error.");
355
+ } catch {
356
+ // ignore logging failures
357
+ }
358
+ }
359
+
360
+ let nextIndexerClient = null;
361
+ try {
362
+ const indexerTransport = new StreamableHTTPClientTransport(indexerUrl);
363
+ const client = new Client(
364
+ {
365
+ name: "ctx-context-engine-bridge-http-client",
366
+ version: "0.0.1",
367
+ },
368
+ {
369
+ capabilities: {
370
+ tools: {},
371
+ resources: {},
372
+ prompts: {},
373
+ },
374
+ },
375
+ );
376
+ await client.connect(indexerTransport);
377
+ nextIndexerClient = client;
378
+ } catch (err) {
379
+ debugLog("[ctxce] Failed to connect MCP HTTP client to indexer: " + String(err));
380
+ nextIndexerClient = null;
381
+ }
382
+
383
+ let nextMemoryClient = null;
384
+ if (memoryUrl) {
385
+ try {
386
+ const memoryTransport = new StreamableHTTPClientTransport(memoryUrl);
387
+ const client = new Client(
388
+ {
389
+ name: "ctx-context-engine-bridge-memory-client",
390
+ version: "0.0.1",
391
+ },
392
+ {
393
+ capabilities: {
394
+ tools: {},
395
+ resources: {},
396
+ prompts: {},
397
+ },
398
+ },
399
+ );
400
+ await client.connect(memoryTransport);
401
+ debugLog(`[ctxce] Connected memory MCP client: ${memoryUrl}`);
402
+ nextMemoryClient = client;
403
+ } catch (err) {
404
+ debugLog("[ctxce] Failed to connect memory MCP client: " + String(err));
405
+ nextMemoryClient = null;
406
+ }
407
+ }
408
+
409
+ indexerClient = nextIndexerClient;
410
+ memoryClient = nextMemoryClient;
411
+
412
+ if (Object.keys(defaultsPayload).length > 1 && indexerClient) {
413
+ await sendSessionDefaults(indexerClient, defaultsPayload, "indexer");
414
+ if (memoryClient) {
415
+ await sendSessionDefaults(memoryClient, defaultsPayload, "memory");
416
+ }
236
417
  }
237
418
  }
238
419
 
420
+ await initializeRemoteClients(false);
421
+
239
422
  const server = new Server( // TODO: marked as depreciated
240
423
  {
241
424
  name: "ctx-context-engine-bridge",
@@ -253,6 +436,10 @@ async function createBridgeServer(options) {
253
436
  let remote;
254
437
  try {
255
438
  debugLog("[ctxce] tools/list: fetching tools from indexer");
439
+ await initializeRemoteClients(false);
440
+ if (!indexerClient) {
441
+ throw new Error("Indexer MCP client not initialized");
442
+ }
256
443
  remote = await withTimeout(
257
444
  indexerClient.listTools(),
258
445
  10000,
@@ -311,21 +498,60 @@ async function createBridgeServer(options) {
311
498
  return indexerResult;
312
499
  }
313
500
 
314
- const targetClient = selectClientForTool(name, indexerClient, memoryClient);
315
- if (!targetClient) {
316
- throw new Error(`Tool ${name} not available on any configured MCP server`);
317
- }
501
+ await initializeRemoteClients(false);
318
502
 
319
503
  const timeoutMs = getBridgeToolTimeoutMs();
320
- const result = await targetClient.callTool(
321
- {
322
- name,
323
- arguments: args,
324
- },
325
- undefined,
326
- { timeout: timeoutMs },
327
- );
328
- return result;
504
+ const maxAttempts = getBridgeRetryAttempts();
505
+ const retryDelayMs = getBridgeRetryDelayMs();
506
+ let sessionRetried = false;
507
+ let lastError;
508
+
509
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
510
+ if (attempt > 0 && retryDelayMs > 0) {
511
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
512
+ }
513
+
514
+ const targetClient = selectClientForTool(name, indexerClient, memoryClient);
515
+ if (!targetClient) {
516
+ throw new Error(`Tool ${name} not available on any configured MCP server`);
517
+ }
518
+
519
+ try {
520
+ const result = await targetClient.callTool(
521
+ {
522
+ name,
523
+ arguments: args,
524
+ },
525
+ undefined,
526
+ { timeout: timeoutMs },
527
+ );
528
+ return result;
529
+ } catch (err) {
530
+ lastError = err;
531
+
532
+ if (isSessionError(err) && !sessionRetried) {
533
+ debugLog(
534
+ "[ctxce] tools/call: detected remote MCP session error; reinitializing clients and retrying once: " +
535
+ String(err),
536
+ );
537
+ await initializeRemoteClients(true);
538
+ sessionRetried = true;
539
+ continue;
540
+ }
541
+
542
+ if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
543
+ throw err;
544
+ }
545
+
546
+ debugLog(
547
+ `[ctxce] tools/call: transient error (attempt ${attempt + 1}/${maxAttempts}), retrying: ` +
548
+ String(err),
549
+ );
550
+ // Loop will retry
551
+ }
552
+ }
553
+
554
+ throw lastError || new Error("Unknown MCP tools/call error");
329
555
  });
330
556
 
331
557
  return server;