Skip to content

Commit b3d8492

Browse files
committed
Add resource limits and cleanup to bridge and extension
Implemented storage and cache size limits, periodic cleanup, and expiry for tokens, codes, and clients in the MCP bridge OAuth handler. Added request body size limit and graceful shutdown to the HTTP MCP server. Improved resource cleanup and cache pruning in the VSCode extension, including limiting skipped auth combos, clearing timers on deactivate, and disposing terminals properly.
1 parent b92daa1 commit b3d8492

8 files changed

Lines changed: 179 additions & 20 deletions

File tree

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)