@context-engine-bridge/context-engine-mcp-bridge 0.0.1

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/bin/ctxce.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli.js";
3
+
4
+ runCli().catch((err) => {
5
+ console.error("[ctxce] Fatal error:", err && err.stack ? err.stack : err);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
+ "version": "0.0.1",
4
+ "description": "Context Engine MCP bridge (stdio proxy combining indexer + memory servers)",
5
+ "bin": {
6
+ "ctxce": "bin/ctxce.js",
7
+ "ctxce-bridge": "bin/ctxce.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node bin/ctxce.js"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.24.3",
15
+ "zod": "^3.25.0"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ }
23
+ }
package/src/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ // CLI entrypoint for ctxce
2
+
3
+ import process from "node:process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { runMcpServer } from "./mcpServer.js";
7
+
8
+ export async function runCli() {
9
+ const argv = process.argv.slice(2);
10
+ const cmd = argv[0];
11
+
12
+ if (cmd === "mcp-serve") {
13
+ // Minimal flag parsing for PoC: allow passing workspace/root and indexer URL.
14
+ // Supported flags:
15
+ // --workspace / --path : workspace root (default: cwd)
16
+ // --indexer-url : override MCP indexer URL (default env CTXCE_INDEXER_URL or http://localhost:8003/mcp)
17
+ const args = argv.slice(1);
18
+ let workspace = process.cwd();
19
+ let indexerUrl = process.env.CTXCE_INDEXER_URL || "http://localhost:8003/mcp";
20
+ let memoryUrl = process.env.CTXCE_MEMORY_URL || null;
21
+
22
+ for (let i = 0; i < args.length; i += 1) {
23
+ const a = args[i];
24
+ if (a === "--workspace" || a === "--path") {
25
+ if (i + 1 < args.length) {
26
+ workspace = args[i + 1];
27
+ i += 1;
28
+ continue;
29
+ }
30
+ }
31
+ if (a === "--indexer-url") {
32
+ if (i + 1 < args.length) {
33
+ indexerUrl = args[i + 1];
34
+ i += 1;
35
+ continue;
36
+ }
37
+ }
38
+ if (a === "--memory-url") {
39
+ if (i + 1 < args.length) {
40
+ memoryUrl = args[i + 1];
41
+ i += 1;
42
+ continue;
43
+ }
44
+ }
45
+ }
46
+
47
+ // eslint-disable-next-line no-console
48
+ console.error(
49
+ `[ctxce] Starting MCP bridge: workspace=${workspace}, indexerUrl=${indexerUrl}, memoryUrl=${memoryUrl || "disabled"}`,
50
+ );
51
+ await runMcpServer({ workspace, indexerUrl, memoryUrl });
52
+ return;
53
+ }
54
+
55
+ // Default help
56
+ const __filename = fileURLToPath(import.meta.url);
57
+ const __dirname = path.dirname(__filename);
58
+ const binName = "ctxce";
59
+
60
+ // eslint-disable-next-line no-console
61
+ console.error(
62
+ `Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>]`,
63
+ );
64
+ process.exit(1);
65
+ }
@@ -0,0 +1,327 @@
1
+ async function sendSessionDefaults(client, payload, label) {
2
+ if (!client) {
3
+ return;
4
+ }
5
+ try {
6
+ await client.callTool({
7
+ name: "set_session_defaults",
8
+ arguments: payload,
9
+ });
10
+ } catch (err) {
11
+ // eslint-disable-next-line no-console
12
+ console.error(`[ctxce] Failed to call set_session_defaults on ${label}:`, err);
13
+ }
14
+ }
15
+ function dedupeTools(tools) {
16
+ const seen = new Set();
17
+ const out = [];
18
+ for (const tool of tools) {
19
+ const key = (tool && typeof tool.name === "string" && tool.name) || "";
20
+ if (!key || seen.has(key)) {
21
+ if (key === "" || key !== "set_session_defaults") {
22
+ continue;
23
+ }
24
+ if (seen.has(key)) {
25
+ continue;
26
+ }
27
+ }
28
+ seen.add(key);
29
+ out.push(tool);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ async function listMemoryTools(client) {
35
+ if (!client) {
36
+ return [];
37
+ }
38
+ try {
39
+ const remote = await client.listTools();
40
+ return Array.isArray(remote?.tools) ? remote.tools.slice() : [];
41
+ } catch (err) {
42
+ // eslint-disable-next-line no-console
43
+ console.error("[ctxce] Error calling memory tools/list:", err);
44
+ return [];
45
+ }
46
+ }
47
+
48
+ function selectClientForTool(name, indexerClient, memoryClient) {
49
+ if (!name) {
50
+ return indexerClient;
51
+ }
52
+ const lowered = name.toLowerCase();
53
+ if (memoryClient && (lowered.startsWith("memory.") || lowered.startsWith("mcp_memory_") || lowered.includes("memory"))) {
54
+ return memoryClient;
55
+ }
56
+ return indexerClient;
57
+ }
58
+ // MCP stdio server implemented using the official MCP TypeScript SDK.
59
+ // Acts as a low-level proxy for tools, forwarding tools/list and tools/call
60
+ // to the remote qdrant-indexer MCP server while adding a local `ping` tool.
61
+
62
+ import process from "node:process";
63
+ import fs from "node:fs";
64
+ import path from "node:path";
65
+ import { execSync } from "node:child_process";
66
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
67
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
68
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
69
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
70
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
71
+
72
+ export async function runMcpServer(options) {
73
+ const workspace = options.workspace || process.cwd();
74
+ const indexerUrl = options.indexerUrl;
75
+ const memoryUrl = options.memoryUrl;
76
+
77
+ const config = loadConfig(workspace);
78
+ const defaultCollection =
79
+ config && typeof config.default_collection === "string"
80
+ ? config.default_collection
81
+ : null;
82
+ const defaultMode =
83
+ config && typeof config.default_mode === "string" ? config.default_mode : null;
84
+ const defaultUnder =
85
+ config && typeof config.default_under === "string" ? config.default_under : null;
86
+
87
+ // eslint-disable-next-line no-console
88
+ console.error(
89
+ `[ctxce] MCP low-level stdio bridge starting: workspace=${workspace}, indexerUrl=${indexerUrl}`,
90
+ );
91
+
92
+ if (defaultCollection) {
93
+ // eslint-disable-next-line no-console
94
+ console.error(
95
+ `[ctxce] Using default collection from ctx_config.json: ${defaultCollection}`,
96
+ );
97
+ }
98
+
99
+ // High-level MCP client for the remote HTTP /mcp indexer
100
+ const indexerTransport = new StreamableHTTPClientTransport(indexerUrl);
101
+ const indexerClient = new Client(
102
+ {
103
+ name: "ctx-context-engine-bridge-http-client",
104
+ version: "0.0.1",
105
+ },
106
+ {
107
+ capabilities: {
108
+ tools: {},
109
+ resources: {},
110
+ prompts: {},
111
+ },
112
+ },
113
+ );
114
+
115
+ try {
116
+ await indexerClient.connect(indexerTransport);
117
+ } catch (err) {
118
+ // eslint-disable-next-line no-console
119
+ console.error("[ctxce] Failed to connect MCP HTTP client to indexer:", err);
120
+ throw err;
121
+ }
122
+
123
+ let memoryClient = null;
124
+ if (memoryUrl) {
125
+ try {
126
+ const memoryTransport = new StreamableHTTPClientTransport(memoryUrl);
127
+ memoryClient = new Client(
128
+ {
129
+ name: "ctx-context-engine-bridge-memory-client",
130
+ version: "0.0.1",
131
+ },
132
+ {
133
+ capabilities: {
134
+ tools: {},
135
+ resources: {},
136
+ prompts: {},
137
+ },
138
+ },
139
+ );
140
+ await memoryClient.connect(memoryTransport);
141
+ // eslint-disable-next-line no-console
142
+ console.error("[ctxce] Connected memory MCP client:", memoryUrl);
143
+ } catch (err) {
144
+ // eslint-disable-next-line no-console
145
+ console.error("[ctxce] Failed to connect memory MCP client:", err);
146
+ memoryClient = null;
147
+ }
148
+ }
149
+
150
+ // Derive a simple session identifier for this bridge process. In the
151
+ // future this can be made user-aware (e.g. from auth), but for now we
152
+ // keep it deterministic per workspace to help the indexer reuse
153
+ // session-scoped defaults.
154
+ const sessionId =
155
+ process.env.CTXCE_SESSION_ID || `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
156
+
157
+ // Best-effort: inform the indexer of default collection and session.
158
+ // If this fails we still proceed, falling back to per-call injection.
159
+ const defaultsPayload = { session: sessionId };
160
+ if (defaultCollection) {
161
+ defaultsPayload.collection = defaultCollection;
162
+ }
163
+ if (defaultMode) {
164
+ defaultsPayload.mode = defaultMode;
165
+ }
166
+ if (defaultUnder) {
167
+ defaultsPayload.under = defaultUnder;
168
+ }
169
+
170
+ if (Object.keys(defaultsPayload).length > 1) {
171
+ await sendSessionDefaults(indexerClient, defaultsPayload, "indexer");
172
+ if (memoryClient) {
173
+ await sendSessionDefaults(memoryClient, defaultsPayload, "memory");
174
+ }
175
+ }
176
+
177
+ const server = new Server( // TODO: marked as depreciated
178
+ {
179
+ name: "ctx-context-engine-bridge",
180
+ version: "0.0.1",
181
+ },
182
+ {
183
+ capabilities: {
184
+ tools: {},
185
+ },
186
+ },
187
+ );
188
+
189
+ // tools/list → fetch tools from remote indexer and append local ping tool
190
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
191
+ let remote;
192
+ try {
193
+ remote = await indexerClient.listTools();
194
+ } catch (err) {
195
+ // eslint-disable-next-line no-console
196
+ console.error("[ctxce] Error calling remote tools/list:", err);
197
+ return { tools: [buildPingTool()] };
198
+ }
199
+
200
+ // eslint-disable-next-line no-console
201
+ console.error("[ctxce] tools/list remote result:", JSON.stringify(remote));
202
+
203
+ const indexerTools = Array.isArray(remote?.tools) ? remote.tools.slice() : [];
204
+ const memoryTools = await listMemoryTools(memoryClient);
205
+ const tools = dedupeTools([...indexerTools, ...memoryTools, buildPingTool()]);
206
+ return { tools };
207
+ });
208
+
209
+ // tools/call → handle ping locally, everything else is proxied to indexer
210
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
211
+ const params = request.params || {};
212
+ const name = params.name;
213
+ let args = params.arguments;
214
+
215
+ if (name === "ping") {
216
+ const branch = detectGitBranch(workspace);
217
+ const text = args && typeof args.text === "string" ? args.text : "pong";
218
+ const suffix = branch ? ` (branch=${branch})` : "";
219
+ return {
220
+ content: [
221
+ {
222
+ type: "text",
223
+ text: `${text}${suffix}`,
224
+ },
225
+ ],
226
+ };
227
+ }
228
+
229
+ // Attach session id so the target server can apply per-session defaults.
230
+ if (sessionId && (args === undefined || args === null || typeof args === "object")) {
231
+ const obj = args && typeof args === "object" ? { ...args } : {};
232
+ if (!Object.prototype.hasOwnProperty.call(obj, "session")) {
233
+ obj.session = sessionId;
234
+ }
235
+ args = obj;
236
+ }
237
+
238
+ if (name === "set_session_defaults") {
239
+ const indexerResult = await indexerClient.callTool({ name, arguments: args });
240
+ if (memoryClient) {
241
+ try {
242
+ await memoryClient.callTool({ name, arguments: args });
243
+ } catch (err) {
244
+ // eslint-disable-next-line no-console
245
+ console.error("[ctxce] Memory set_session_defaults failed:", err);
246
+ }
247
+ }
248
+ return indexerResult;
249
+ }
250
+
251
+ const targetClient = selectClientForTool(name, indexerClient, memoryClient);
252
+ if (!targetClient) {
253
+ throw new Error(`Tool ${name} not available on any configured MCP server`);
254
+ }
255
+
256
+ const result = await targetClient.callTool({
257
+ name,
258
+ arguments: args,
259
+ });
260
+ return result;
261
+ });
262
+
263
+ const transport = new StdioServerTransport();
264
+ await server.connect(transport);
265
+ }
266
+
267
+ function loadConfig(startDir) {
268
+ try {
269
+ let dir = startDir;
270
+ for (let i = 0; i < 5; i += 1) {
271
+ const cfgPath = path.join(dir, "ctx_config.json");
272
+ if (fs.existsSync(cfgPath)) {
273
+ try {
274
+ const raw = fs.readFileSync(cfgPath, "utf8");
275
+ const parsed = JSON.parse(raw);
276
+ if (parsed && typeof parsed === "object") {
277
+ return parsed;
278
+ }
279
+ } catch (err) {
280
+ // eslint-disable-next-line no-console
281
+ console.error("[ctxce] Failed to parse ctx_config.json:", err);
282
+ return null;
283
+ }
284
+ }
285
+ const parent = path.dirname(dir);
286
+ if (!parent || parent === dir) {
287
+ break;
288
+ }
289
+ dir = parent;
290
+ }
291
+ } catch (err) {
292
+ // eslint-disable-next-line no-console
293
+ console.error("[ctxce] Error while loading ctx_config.json:", err);
294
+ }
295
+ return null;
296
+ }
297
+
298
+ function buildPingTool() {
299
+ return {
300
+ name: "ping",
301
+ description: "Basic ping tool exposed by the ctx bridge",
302
+ inputSchema: {
303
+ type: "object",
304
+ properties: {
305
+ text: {
306
+ type: "string",
307
+ description: "Optional text to echo back.",
308
+ },
309
+ },
310
+ required: [],
311
+ },
312
+ };
313
+ }
314
+
315
+ function detectGitBranch(workspace) {
316
+ try {
317
+ const out = execSync("git rev-parse --abbrev-ref HEAD", {
318
+ cwd: workspace,
319
+ stdio: ["ignore", "pipe", "ignore"],
320
+ });
321
+ const name = out.toString("utf8").trim();
322
+ return name || null;
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
327
+