@context-engine-bridge/context-engine-mcp-bridge 0.0.5 → 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 +212 -0
- package/package.json +1 -1
- package/src/authCli.js +206 -0
- package/src/authConfig.js +84 -0
- package/src/cli.js +9 -1
- package/src/mcpServer.js +49 -2
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
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
|
@@ -247,6 +247,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
247
247
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
248
248
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
249
249
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
250
|
+
import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
|
|
250
251
|
|
|
251
252
|
async function createBridgeServer(options) {
|
|
252
253
|
const workspace = options.workspace || process.cwd();
|
|
@@ -281,8 +282,54 @@ async function createBridgeServer(options) {
|
|
|
281
282
|
// future this can be made user-aware (e.g. from auth), but for now we
|
|
282
283
|
// keep it deterministic per workspace to help the indexer reuse
|
|
283
284
|
// session-scoped defaults.
|
|
284
|
-
const
|
|
285
|
-
|
|
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
|
+
}
|
|
286
333
|
|
|
287
334
|
// Best-effort: inform the indexer of default collection and session.
|
|
288
335
|
// If this fails we still proceed, falling back to per-call injection.
|