Skip to content

Commit a740142

Browse files
committed
Merge branch 'test' of https://github.com/m1rl0k/Context-Engine into test
2 parents 3d04e3a + 24f5be6 commit a740142

9 files changed

Lines changed: 181 additions & 20 deletions

File tree

.github/workflows/publish-cli.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ jobs:
6666
env:
6767
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6868
run: npm publish --access public --provenance
69+
env:
70+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

ctx-mcp-bridge/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@context-engine-bridge/context-engine-mcp-bridge",
3-
"version": "0.0.13",
3+
"version": "0.0.14",
44
"description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
55
"bin": {
66
"ctxce": "bin/ctxce.js",

ctx-mcp-bridge/src/mcpServer.js

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -502,10 +502,23 @@ async function createBridgeServer(options) {
502502
}
503503

504504
if (forceRecreate) {
505-
try {
506-
debugLog("[ctxce] Reinitializing remote MCP clients after session error.");
507-
} catch {
508-
// ignore logging failures
505+
debugLog("[ctxce] Reinitializing remote MCP clients after session error.");
506+
507+
if (indexerClient) {
508+
try {
509+
await indexerClient.close();
510+
} catch {
511+
// ignore close errors
512+
}
513+
indexerClient = null;
514+
}
515+
if (memoryClient) {
516+
try {
517+
await memoryClient.close();
518+
} catch {
519+
// ignore close errors
520+
}
521+
memoryClient = null;
509522
}
510523
}
511524

@@ -830,11 +843,28 @@ export async function runHttpMcpServer(options) {
830843
return;
831844
}
832845

846+
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
833847
let body = "";
848+
let bodyLimitExceeded = false;
834849
req.on("data", (chunk) => {
850+
if (bodyLimitExceeded) return;
835851
body += chunk;
852+
if (body.length > MAX_BODY_SIZE) {
853+
bodyLimitExceeded = true;
854+
req.destroy();
855+
res.statusCode = 413;
856+
res.setHeader("Content-Type", "application/json");
857+
res.end(
858+
JSON.stringify({
859+
jsonrpc: "2.0",
860+
error: { code: -32000, message: "Request body too large" },
861+
id: null,
862+
}),
863+
);
864+
}
836865
});
837866
req.on("end", async () => {
867+
if (bodyLimitExceeded) return;
838868
let parsed;
839869
try {
840870
parsed = body ? JSON.parse(body) : {};
@@ -902,6 +932,25 @@ export async function runHttpMcpServer(options) {
902932
httpServer.listen(port, '127.0.0.1', () => {
903933
debugLog(`[ctxce] HTTP MCP bridge listening on 127.0.0.1:${port}`);
904934
});
935+
936+
let shuttingDown = false;
937+
const shutdown = (signal) => {
938+
if (shuttingDown) return;
939+
shuttingDown = true;
940+
debugLog(`[ctxce] Received ${signal}; closing HTTP server (waiting for in-flight requests).`);
941+
httpServer.close(() => {
942+
debugLog("[ctxce] HTTP server closed.");
943+
process.exit(0);
944+
});
945+
const SHUTDOWN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes for long MCP calls
946+
setTimeout(() => {
947+
debugLog("[ctxce] Forcing exit after shutdown timeout.");
948+
process.exit(1);
949+
}, SHUTDOWN_TIMEOUT_MS).unref();
950+
};
951+
952+
process.on("SIGTERM", () => shutdown("SIGTERM"));
953+
process.on("SIGINT", () => shutdown("SIGINT"));
905954
}
906955

907956
function loadConfig(startDir) {

ctx-mcp-bridge/src/oauthHandler.js

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,97 @@ const pendingCodes = new Map();
1616
// Maps client_id to client info
1717
const registeredClients = new Map();
1818

19+
// ============================================================================
20+
// Storage Limits and Cleanup Configuration
21+
// ============================================================================
22+
23+
const MAX_TOKEN_STORE_SIZE = 10000;
24+
const MAX_PENDING_CODES_SIZE = 1000;
25+
const MAX_REGISTERED_CLIENTS_SIZE = 1000;
26+
const TOKEN_EXPIRY_MS = 86400000; // 24 hours
27+
const CODE_EXPIRY_MS = 600000; // 10 minutes
28+
const CLIENT_EXPIRY_MS = 7 * 86400000; // 7 days
29+
const CLEANUP_INTERVAL_MS = 300000; // 5 minutes
30+
31+
// Cleanup interval reference (for cleanup on shutdown if needed)
32+
let cleanupIntervalId = null;
33+
1934
// ============================================================================
2035
// OAuth Utilities
2136
// ============================================================================
2237

23-
/**
24-
* Clean up expired tokens from tokenStore
25-
* Called periodically to prevent unbounded memory growth
26-
*/
2738
function cleanupExpiredTokens() {
2839
const now = Date.now();
29-
const expiryMs = 86400000; // 24 hours
3040
for (const [token, data] of tokenStore.entries()) {
31-
if (now - data.createdAt > expiryMs) {
41+
if (now - data.createdAt > TOKEN_EXPIRY_MS) {
3242
tokenStore.delete(token);
3343
}
3444
}
3545
}
3646

47+
function cleanupExpiredCodes() {
48+
const now = Date.now();
49+
for (const [code, data] of pendingCodes.entries()) {
50+
if (now - data.createdAt > CODE_EXPIRY_MS) {
51+
pendingCodes.delete(code);
52+
}
53+
}
54+
}
55+
56+
function cleanupExpiredClients() {
57+
const now = Date.now();
58+
for (const [clientId, data] of registeredClients.entries()) {
59+
if (now - data.createdAt > CLIENT_EXPIRY_MS) {
60+
registeredClients.delete(clientId);
61+
}
62+
}
63+
}
64+
65+
function enforceStorageLimits() {
66+
if (tokenStore.size > MAX_TOKEN_STORE_SIZE) {
67+
const entries = [...tokenStore.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
68+
const toRemove = entries.slice(0, tokenStore.size - MAX_TOKEN_STORE_SIZE);
69+
for (const [key] of toRemove) {
70+
tokenStore.delete(key);
71+
}
72+
}
73+
if (pendingCodes.size > MAX_PENDING_CODES_SIZE) {
74+
const entries = [...pendingCodes.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
75+
const toRemove = entries.slice(0, pendingCodes.size - MAX_PENDING_CODES_SIZE);
76+
for (const [key] of toRemove) {
77+
pendingCodes.delete(key);
78+
}
79+
}
80+
if (registeredClients.size > MAX_REGISTERED_CLIENTS_SIZE) {
81+
const entries = [...registeredClients.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
82+
const toRemove = entries.slice(0, registeredClients.size - MAX_REGISTERED_CLIENTS_SIZE);
83+
for (const [key] of toRemove) {
84+
registeredClients.delete(key);
85+
}
86+
}
87+
}
88+
89+
function runPeriodicCleanup() {
90+
cleanupExpiredTokens();
91+
cleanupExpiredCodes();
92+
cleanupExpiredClients();
93+
enforceStorageLimits();
94+
}
95+
96+
export function startCleanupInterval() {
97+
if (!cleanupIntervalId) {
98+
cleanupIntervalId = setInterval(runPeriodicCleanup, CLEANUP_INTERVAL_MS);
99+
cleanupIntervalId.unref?.();
100+
}
101+
}
102+
103+
export function stopCleanupInterval() {
104+
if (cleanupIntervalId) {
105+
clearInterval(cleanupIntervalId);
106+
cleanupIntervalId = null;
107+
}
108+
}
109+
37110
function generateToken() {
38111
return randomBytes(32).toString("hex");
39112
}
@@ -545,20 +618,14 @@ export function handleOAuthToken(req, res) {
545618
});
546619
}
547620

548-
/**
549-
* Validate Bearer token and return session info
550-
* @param {string} token - Bearer token
551-
* @returns {{sessionId: string, backendUrl: string} | null}
552-
*/
553621
export function validateBearerToken(token) {
554622
const tokenData = tokenStore.get(token);
555623
if (!tokenData) {
556624
return null;
557625
}
558626

559-
// Check token age (24 hour expiry)
560627
const tokenAge = Date.now() - tokenData.createdAt;
561-
if (tokenAge > 86400000) {
628+
if (tokenAge > TOKEN_EXPIRY_MS) {
562629
tokenStore.delete(token);
563630
return null;
564631
}
@@ -583,3 +650,5 @@ export function isOAuthEndpoint(pathname) {
583650
pathname === "/oauth/token"
584651
);
585652
}
653+
654+
startCleanupInterval();

vscode-extension/context-engine-uploader/auth_utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const process = require('process');
22

33
const _skippedAuthCombos = new Set();
4+
const _SKIPPED_AUTH_MAX_SIZE = 100;
45

56
function getFetch(deps) {
67
if (deps && typeof deps.fetchGlobal === 'function') {
@@ -110,6 +111,10 @@ async function ensureAuthIfRequired(endpoint, deps) {
110111
'Skip for now',
111112
);
112113
if (choice !== 'Sign In') {
114+
if (_skippedAuthCombos.size >= _SKIPPED_AUTH_MAX_SIZE) {
115+
const first = _skippedAuthCombos.values().next().value;
116+
if (first) _skippedAuthCombos.delete(first);
117+
}
113118
_skippedAuthCombos.add(skipKey);
114119
return;
115120
}

vscode-extension/context-engine-uploader/extension.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,10 @@ async function writeCtxConfig() {
13541354
}
13551355
}
13561356
function deactivate() {
1357+
if (pendingProfileRestartTimer) {
1358+
clearTimeout(pendingProfileRestartTimer);
1359+
pendingProfileRestartTimer = undefined;
1360+
}
13571361
disposeIndexedWatcher();
13581362
return Promise.all([stopProcesses()]);
13591363
}

vscode-extension/context-engine-uploader/logs_terminal.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ function createLogsTerminalManager(deps) {
4242
function dispose() {
4343
try {
4444
logTailActive = false;
45-
logsTerminal = undefined;
45+
if (logsTerminal) {
46+
logsTerminal.dispose();
47+
logsTerminal = undefined;
48+
}
4649
} catch (_) {
4750
// ignore
4851
}

vscode-extension/context-engine-uploader/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "context-engine-uploader",
33
"displayName": "Context Engine Uploader",
44
"description": "Runs the Context-Engine remote upload client with a force sync on startup followed by watch mode. Requires Python with pip install requests urllib3 charset_normalizer.",
5-
"version": "0.1.39",
5+
"version": "0.1.40",
66
"publisher": "context-engine",
77
"engines": {
88
"vscode": "^1.85.0"

vscode-extension/context-engine-uploader/sidebar.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,28 @@ function findConfigFile(bases, filename) {
8585
const _authStatusCache = new Map();
8686
const _AUTH_STATUS_TTL_MS = 30_000;
8787
const _AUTH_STATUS_TIMEOUT_MS = 750;
88+
const _AUTH_STATUS_MAX_ENTRIES = 100;
8889

8990
const _endpointReachableCache = new Map();
9091
const _ENDPOINT_REACHABLE_TTL_MS = 15_000;
9192
const _ENDPOINT_REACHABLE_TIMEOUT_MS = 750;
93+
const _ENDPOINT_REACHABLE_MAX_ENTRIES = 100;
94+
95+
function pruneCache(cache, ttlMs, maxEntries) {
96+
const now = Date.now();
97+
for (const [key, entry] of cache.entries()) {
98+
if (!entry.ts || (now - entry.ts) > ttlMs) {
99+
cache.delete(key);
100+
}
101+
}
102+
if (cache.size > maxEntries) {
103+
const entries = [...cache.entries()].sort((a, b) => (a[1].ts || 0) - (b[1].ts || 0));
104+
const toRemove = entries.slice(0, cache.size - maxEntries);
105+
for (const [key] of toRemove) {
106+
cache.delete(key);
107+
}
108+
}
109+
}
92110

93111
async function probeEndpointReachable(endpoint) {
94112
const base = (endpoint || '').trim().replace(/\/+$/, '');
@@ -613,6 +631,8 @@ function register(context, deps) {
613631
profilesProvider.refresh();
614632
statusProvider.refresh();
615633
actionsProvider.refresh();
634+
pruneCache(_authStatusCache, _AUTH_STATUS_TTL_MS, _AUTH_STATUS_MAX_ENTRIES);
635+
pruneCache(_endpointReachableCache, _ENDPOINT_REACHABLE_TTL_MS, _ENDPOINT_REACHABLE_MAX_ENTRIES);
616636
}, 2000);
617637
}
618638
};
@@ -621,6 +641,15 @@ function register(context, deps) {
621641
statusTree.onDidChangeVisibility(bumpTimer, null, context.subscriptions);
622642
actionsTree.onDidChangeVisibility(bumpTimer, null, context.subscriptions);
623643

644+
context.subscriptions.push({
645+
dispose: () => {
646+
if (refreshTimer) {
647+
clearInterval(refreshTimer);
648+
refreshTimer = undefined;
649+
}
650+
}
651+
});
652+
624653
bumpTimer();
625654

626655
return {

0 commit comments

Comments
 (0)