@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 +7 -0
- package/package.json +23 -0
- package/src/cli.js +65 -0
- package/src/mcpServer.js +327 -0
package/bin/ctxce.js
ADDED
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
|
+
}
|
package/src/mcpServer.js
ADDED
|
@@ -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
|
+
|