Container Configuration: - Added network_mode and port_bindings to ContainerConfig - Support for 'none', 'host', and 'bridge' network modes - Default remains 'none' for security Jupyter Kernel Manager: - Dynamic port allocation for 5 ZMQ channels using socket.socket() - _allocate_ports() finds available ports via OS binding - Host networking mode for Jupyter kernels (network_mode='host') - Connection file properly mounted into container - Port bindings tracked for documentation Kernel Readiness: - _wait_for_kernel_ready() polls shell port until kernel responds - Configurable timeout (30s) and poll interval (0.5s) - Replaced time.sleep(2) with proper connectivity check - Early return when kernel is ready This completes the core ZMQ communication infrastructure needed for real Jupyter kernel operation.
8.2 KiB
Jupyter Backend Implementation Status
Branch
feature/real-jupyter-backend
Architecture Decision
Decision: 1:1 mapping - one container per session, one kernel per container
Rationale:
- Strongest isolation (processes, namespaces, resources)
- Easier debugging and resource tracking
- Clean lifecycle management
- Optional shared volumes for file exchange between sessions
Components:
jupyter-client: Runs on host (MCP-Forge server)ipykernel: Runs in containers- Communication: ZMQ protocol via Jupyter message protocol
Implementation Progress
✅ Completed
-
Core Architecture (kernel.py)
- Replaced mock exec() implementation with real Jupyter protocol
JupyterKernelManagerclass with proper ZMQ communicationKernelInfodataclass with connection info and client- Connection file generation
- Jupyter message protocol for code execution (execute_request, stream, execute_result)
-
Kernel Lifecycle
start_kernel(): Container creation, kernel startup, client connectionshutdown_kernel(): Graceful shutdown, container cleanup, connection file removalrestart_kernel(): Stub implementation (needs KernelManager integration)
-
Code Execution
execute_code(): Proper message protocol execution- Output collection (stdout, stderr, result)
- Error handling with traceback capture
- Activity timestamp tracking
-
Namespace Introspection
inspect_namespace(): List variables via introspection codeget_variable_info(): Type, size, shape, repr extraction
-
Resource Management
cleanup_idle_kernels(): Time-based cleanup- Activity timestamp updates
-
MCP Integration
start_kernel()acceptsinjection_codeandbridge_socket_path- Bridge socket mounted as volume in container (Unix domain socket)
- Injection code executed once on kernel startup (silent, no history)
- Error handling for injection failures
- Passed through SessionManager and JupyterBackend
-
Network & Port Management
- Host networking mode for ZMQ communication (
network_mode="host") - Dynamic port allocation for 5 ZMQ channels (shell, iopub, stdin, control, hb)
- Port availability checking before allocation
- Port bindings tracked in ContainerConfig
- Connection file mounted into container at
/tmp/kernel-{id}.json
- Host networking mode for ZMQ communication (
-
Kernel Readiness
_wait_for_kernel_ready(): Polls shell port until kernel responds- Configurable timeout (default 30s) and poll interval (0.5s)
- Replaces simple sleep with proper port connectivity check
- Returns early when kernel is ready
-
Dependencies
jupyter-client>=8.8.0added to server dependenciesipykernelremoved from server (will be in container image)pyzmq>=27.1.0for ZMQ support
-
Documentation
- architecture1.md updated with Jupyter Backend section
- todo.md Phase 2.2.1 updated with architecture details
- Architecture flow diagram and session mapping explanation
- JUPYTER_IMPLEMENTATION_STATUS.md tracking document
🚧 In Progress / TODO
-
Restart Implementation
- Use
KernelManagerinstead of justBlockingKernelClient - Proper restart via
KernelManager.restart_kernel() - Handle restart failures gracefully
- Use
-
Container Image
- Add
ipykerneltomcp-forge/jupyter:latestimage - Configure kernel to accept ZMQ connections
- Test kernel startup in isolation
- Add
-
Testing
- Update tests in
tests/execution/jupyter/test_kernel.py - Mock ZMQ connection and Jupyter messages
- Mock container manager for unit tests
- Integration tests with real containers
- Test port conflict handling
- Test connection file cleanup
- Update tests in
-
Error Handling
- Port conflict resolution
- Kernel crash detection and recovery
- Connection timeout handling
- Invalid connection file handling
-
Performance
- Connection pooling/reuse consideration
- Batch message processing
- Async execution option
Test Status
Current test results: 20 failed, 2 passed
Failure Reason: Tests attempt to start real Jupyter kernels, but implementation is missing:
- Port mappings for ZMQ
- Connection file mounting
- Proper readiness check
Required Actions:
- Complete container configuration (ports, volumes)
- Update container image to include ipykernel
- Mock ZMQ connections in unit tests
- Create integration test suite with real containers
Known Issues
-
Port Assignment: Currently using
port: 0(let ZMQ assign), but need to:- Get assigned ports after socket binding
- Map container ports to host ports
- Update connection info with actual ports
-
Connection File: Generated on host, but needs to be:
- Mounted into container at known path
- Have correct host-accessible addresses
-
Network Mode: Need to determine:
- Host network mode (simpler, less isolated)
- Bridge network with port mapping (more isolated, more complex)
-
Restart Logic:
restart_kernel()only updates timestamp:- Need KernelManager integration
- Or implement shutdown + start approach
Next Steps
-
Immediate (Core functionality):
1. Update container image with ipykernel 2. Test kernel startup with real container 3. Mock tests for unit testing -
Short-term (Stability):
4. Implement proper kernel restart 5. Error handling improvements 6. Port conflict handling -
Medium-term (Production-ready):
7. Implement restart via KernelManager 8. Integration test suite 9. Error handling improvements 10. Performance testing
Architecture Diagrams
Communication Flow
┌─────────────────────┐
│ MCP-Forge Server │
│ (Host Process) │
│ │
│ ┌─────────────────┐ │
│ │ jupyter-client │ │
│ │ │ │
│ │ BlockingKernel │ │
│ │ Client │ │
│ └────────┬────────┘ │
│ │ │
│ │ ZMQ │
│ │ (shell, │
│ │ iopub, │
│ │ stdin, │
│ │ control,│
│ │ hb) │
└──────────┼──────────┘
│
│ TCP ports
│ (mapped)
│
┌──────────┼──────────┐
│ Container│ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ ipykernel │ │
│ │ │ │
│ │ Python REPL │ │
│ │ + ZMQ Server │ │
│ └─────────────────┘ │
│ │
│ Connection file: │
│ /tmp/kernel-X.json │
└─────────────────────┘
Session-to-Kernel Mapping
Session A ──> Container A ──> Kernel A ──> Namespace A
Session B ──> Container B ──> Kernel B ──> Namespace B
Session C ──> Container C ──> Kernel C ──> Namespace C
Optional: Shared volume for file exchange
Container A ──┐
├──> /shared/session-group-1
Container B ──┘
Container C ──> /shared/session-group-2 (independent)
References
Commit History
244a3e5- Implement real Jupyter backend with jupyter-client- Replaced mock exec() with real Jupyter protocol
- Added ZMQ communication
- Kernel lifecycle management
- Backed up old implementation
Old Implementation
The previous mock implementation using exec() is preserved in:
src/mcp_forge/execution/jupyter/kernel_old.py
This can be used for reference or comparison.