@@ -74,6 +74,7 @@ export class ProcessSpawner {
7474 const commandPaths : Record < string , string > = {
7575 'npx' : '/usr/bin/npx' ,
7676 'node' : '/usr/bin/node' ,
77+ 'uvx' : '/usr/bin/uvx' ,
7778 'python' : '/usr/bin/python' ,
7879 'python3' : '/usr/bin/python3'
7980 } ;
@@ -92,6 +93,49 @@ export class ProcessSpawner {
9293 return `/usr/bin/${ command } ` ;
9394 }
9495
96+ /**
97+ * Get runtime-specific environment variables for nsjail isolation
98+ * Different runtimes need different cache directories and package manager settings
99+ */
100+ private getEnvironmentForRuntime ( config : MCPServerConfig ) : string [ ] {
101+ const runtime = config . runtime || 'node' ; // Default to node for backward compatibility
102+ const envVars : string [ ] = [ ] ;
103+
104+ switch ( runtime ) {
105+ case 'node' :
106+ envVars . push (
107+ '-E' , 'HOME=/home/node' ,
108+ '-E' , 'PATH=/usr/bin:/bin:/usr/local/bin' ,
109+ '-E' , 'NPM_CONFIG_CACHE=/home/node/.npm' ,
110+ '-E' , 'NPM_CONFIG_PREFIX=/home/node/.npm-global' ,
111+ '-E' , 'NPM_CONFIG_UPDATE_NOTIFIER=false' ,
112+ '-E' , 'NO_UPDATE_NOTIFIER=1'
113+ ) ;
114+ break ;
115+
116+ case 'python' :
117+ envVars . push (
118+ '-E' , 'HOME=/home/python' ,
119+ '-E' , 'PATH=/usr/bin:/bin:/usr/local/bin' ,
120+ '-E' , 'UV_CACHE_DIR=/home/python/.cache/uv' ,
121+ '-E' , 'UV_TOOL_DIR=/home/python/.local/bin' ,
122+ '-E' , 'PYTHONUNBUFFERED=1' ,
123+ '-E' , 'UV_NO_UPDATE_NOTIFIER=1'
124+ ) ;
125+ break ;
126+
127+ default :
128+ // Generic runtime - minimal environment
129+ envVars . push (
130+ '-E' , `HOME=/home/${ runtime } ` ,
131+ '-E' , 'PATH=/usr/bin:/bin:/usr/local/bin'
132+ ) ;
133+ break ;
134+ }
135+
136+ return envVars ;
137+ }
138+
95139 /**
96140 * Spawn a process - routes to direct or nsjail based on environment
97141 */
@@ -120,31 +164,34 @@ export class ProcessSpawner {
120164 }
121165
122166 /**
123- * Ensure team-specific cache directory exists
167+ * Ensure team-specific cache directory exists for the runtime
124168 */
125- async ensureCacheDirectory ( teamId : string ) : Promise < string > {
126- const cacheDir = `${ mcpCacheBaseDir } /mcp-cache/${ teamId } ` ;
169+ async ensureCacheDirectory ( teamId : string , runtime : string ) : Promise < string > {
170+ const cacheDir = `${ mcpCacheBaseDir } /mcp-cache/${ runtime } / ${ teamId } ` ;
127171
128172 if ( ! existsSync ( cacheDir ) ) {
129173 this . logger . info ( {
130174 operation : 'create_cache_directory' ,
131175 team_id : teamId ,
176+ runtime : runtime ,
132177 cache_dir : cacheDir
133- } , `Creating team cache directory: ${ cacheDir } ` ) ;
178+ } , `Creating team cache directory for ${ runtime } runtime : ${ cacheDir } ` ) ;
134179
135180 try {
136181 await mkdir ( cacheDir , { recursive : true } ) ;
137182
138183 this . logger . info ( {
139184 operation : 'cache_directory_created' ,
140185 team_id : teamId ,
186+ runtime : runtime ,
141187 cache_dir : cacheDir
142188 } , `Team cache directory created successfully` ) ;
143189 } catch ( error ) {
144190 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
145191 this . logger . error ( {
146192 operation : 'cache_directory_creation_failed' ,
147193 team_id : teamId ,
194+ runtime : runtime ,
148195 cache_dir : cacheDir ,
149196 error : errorMessage
150197 } , `Failed to create team cache directory` ) ;
@@ -158,30 +205,34 @@ export class ProcessSpawner {
158205 /**
159206 * Spawn process with nsjail isolation (production mode on Linux)
160207 *
161- * Configuration based on empirical testing with npx and Node.js:
162- * - Memory: 2048MB (V8 minimum requirement )
163- * - Processes: 1000 (npm spawns many child processes)
208+ * Configuration supports multiple runtimes ( Node.js, Python, etc.) :
209+ * - Memory: 2048MB (adequate for V8 and Python interpreters )
210+ * - Processes: 1000 (package managers spawn many child processes)
164211 * - File descriptors: 1024 (adequate for I/O operations)
165212 * - File size: 50MB (prevents oversized downloads)
166- * - /dev files: Required for Node.js crypto and I/O operations
213+ * - /dev files: Required for crypto and I/O operations
167214 * - --proc_rw: Required for pthread_create and thread management
168215 */
169216 async spawnWithNsjail ( config : MCPServerConfig ) : Promise < ChildProcess > {
217+ // Determine runtime (default to 'node' for backward compatibility)
218+ const runtime = config . runtime || 'node' ;
219+
170220 // Ensure team-specific cache directory exists before mounting
171- const cacheDir = await this . ensureCacheDirectory ( config . team_id ) ;
221+ const cacheDir = await this . ensureCacheDirectory ( config . team_id , runtime ) ;
172222
173223 this . logger . info ( {
174224 operation : 'spawn_nsjail' ,
175225 installation_name : config . installation_name ,
176226 team_id : config . team_id ,
227+ runtime : runtime ,
177228 cache_dir : cacheDir ,
178229 memory_limit_mb : nsjailConfig . memoryLimitMB ,
179230 cpu_time_limit_seconds : nsjailConfig . cpuTimeLimitSeconds ,
180231 max_processes : nsjailConfig . maxProcesses ,
181232 max_open_files : nsjailConfig . maxOpenFiles ,
182233 max_file_size_mb : nsjailConfig . maxFileSizeMB ,
183234 tmpfs_size : nsjailConfig . tmpfsSize
184- } , ' Spawning process with nsjail isolation' ) ;
235+ } , ` Spawning ${ runtime } MCP server with nsjail isolation` ) ;
185236
186237 // Get current user UID and GID (deploystack user in production)
187238 const uid = process . getuid ? process . getuid ( ) : 1000 ;
@@ -199,12 +250,12 @@ export class ProcessSpawner {
199250 // Build nsjail arguments based on working production configuration
200251 const nsjailArgs = [
201252 '-Mo' , // Mount mode: once, don't remount
202- '--proc_rw' , // Required for Node.js pthread_create
253+ '--proc_rw' , // Required for pthread_create and thread management
203254 '--user' , String ( uid ) , // Use current user (deploystack)
204255 '--group' , String ( gid ) , // Use current group (deploystack)
205- '--rlimit_as' , String ( nsjailConfig . memoryLimitMB ) , // Memory limit (MB) - 2048 minimum for V8
256+ '--rlimit_as' , String ( nsjailConfig . memoryLimitMB ) , // Memory limit (MB) - 2048 for interpreters
206257 '--rlimit_cpu' , String ( nsjailConfig . cpuTimeLimitSeconds ) , // CPU time limit (seconds)
207- '--rlimit_nproc' , String ( nsjailConfig . maxProcesses ) , // Max processes - 1000 for npm
258+ '--rlimit_nproc' , String ( nsjailConfig . maxProcesses ) , // Max processes - 1000 for package managers
208259 '--rlimit_nofile' , String ( nsjailConfig . maxOpenFiles ) , // Max file descriptors
209260 '--rlimit_fsize' , String ( nsjailConfig . maxFileSizeMB ) , // Max file size (MB)
210261 '--time_limit' , '0' , // No wall-clock time limit
@@ -215,25 +266,21 @@ export class ProcessSpawner {
215266 '-R' , '/sbin' , // Read-only mount: /sbin
216267 '-R' , '/etc' , // Read-only mount: /etc (includes resolv.conf)
217268 '-T' , `/tmp:size=${ nsjailConfig . tmpfsSize } ` , // Writable temp with size limit (100M)
218- '-B' , `${ cacheDir } :/home/npx ` , // Team -specific cache directory mount
269+ '-B' , `${ cacheDir } :/home/${ runtime } ` , // Runtime -specific cache directory mount
219270 '--bindmount' , '/dev/null:/dev/null' , // Required for I/O redirection
220271 '--bindmount' , '/dev/urandom:/dev/urandom' , // Required for crypto operations
221272 '--bindmount' , '/dev/zero:/dev/zero' , // Required for memory allocation
222273 '--symlink' , '/proc/self/fd:/dev/fd' , // Required for file descriptor management
223- '-E' , 'HOME=/home/npx' , // Set HOME for npx cache
224- '-E' , 'PATH=/usr/bin:/bin:/usr/local/bin' , // Set PATH
225- '-E' , 'NPM_CONFIG_CACHE=/home/npx/.npm' , // npm cache location
226- '-E' , 'NPM_CONFIG_PREFIX=/home/npx/.npm-global' , // npm global prefix
227- '-E' , 'NPM_CONFIG_UPDATE_NOTIFIER=false' , // Disable update notifier
228- '-E' , 'NO_UPDATE_NOTIFIER=1' , // Disable update notifier (alternative)
274+ // Runtime-specific environment variables
275+ ...this . getEnvironmentForRuntime ( config ) ,
229276 // Inject user-provided environment variables (sanitized)
230277 ...this . sanitizeEnvVars ( config . env , config . installation_name ) ,
231- '--disable_clone_newnet' , // Allow network access (required for npm downloads)
278+ '--disable_clone_newnet' , // Allow network access (required for package downloads)
232279 '--disable_clone_newcgroup' , // Disable cgroup namespace (causes clone() errors on some kernels)
233280 '--disable_no_new_privs' , // May be needed for some packages
234281 '--hostname' , `mcp-${ config . team_id } ` , // Team-specific hostname
235282 '--' , // End of nsjail args
236- fullCommandPath , // MCP server command with full path (e.g., /usr/bin/npx)
283+ fullCommandPath , // MCP server command with full path (e.g., /usr/bin/npx or /usr/bin/uvx )
237284 ...config . args // MCP server arguments
238285 ] ;
239286
0 commit comments