1010from jupyter_server_client import JupyterServerClient
1111from jupyter_mcp_server .tools ._base import BaseTool , ServerMode
1212from jupyter_mcp_server .notebook_manager import NotebookManager
13- from jupyter_mcp_server .utils import get_current_notebook_context , get_jupyter_ydoc , clean_notebook_outputs
13+ from jupyter_mcp_server .utils import get_current_notebook_context , get_notebook_model , clean_notebook_outputs
1414from jupyter_mcp_server .models import Notebook
1515
1616
17+
1718class InsertCellTool (BaseTool ):
1819 """Tool to insert a cell at a specified position."""
1920
21+ def _validate_cell_insertion_params (
22+ self ,
23+ cell_index : int ,
24+ total_cells : int ,
25+ cell_type : str
26+ ) -> int :
27+ """Validate and normalize cell insertion parameters.
28+
29+ Args:
30+ cell_index: Target index for insertion (-1 for append)
31+ total_cells: Total number of cells in the notebook
32+ cell_type: Type of cell to insert
33+
34+ Returns:
35+ Normalized actual_index for insertion
36+
37+ Raises:
38+ IndexError: When cell_index is out of valid range
39+ ValueError: When cell_type is invalid
40+ """
41+ if cell_index < - 1 or cell_index > total_cells :
42+ raise IndexError (
43+ f"Index { cell_index } is outside valid range [-1, { total_cells } ]. "
44+ f"Use -1 to append at end."
45+ )
46+
47+ # Normalize -1 to append position
48+ actual_index = cell_index if cell_index != - 1 else total_cells
49+ return actual_index
50+
2051 async def _insert_cell_ydoc (
2152 self ,
2253 serverapp : Any ,
2354 notebook_path : str ,
2455 cell_index : int ,
2556 cell_type : Literal ["code" , "markdown" ],
2657 cell_source : str
27- ) -> tuple [int , int ]:
58+ ) -> tuple [Notebook , int , int ]:
2859 """Insert cell using YDoc (collaborative editing mode).
2960
3061 Args:
3162 serverapp: Jupyter ServerApp instance
3263 notebook_path: Path to the notebook
3364 cell_index: Index to insert at (-1 for append)
34- cell_type: Type of cell to insert
65+ cell_type: Type of cell to insert ("code", "markdown")
3566 cell_source: Source content for the cell
3667
3768 Returns:
38- Success message with surrounding cells info
69+ Tuple of (notebook, actual_index, total_cells_after_insertion)
70+
71+ Raises:
72+ IndexError: When cell_index is out of range
3973 """
40- # Get file_id from file_id_manager
41- file_id_manager = serverapp .web_app .settings .get ("file_id_manager" )
42- if file_id_manager is None :
43- raise RuntimeError ("file_id_manager not available in serverapp" )
74+ nb = await get_notebook_model (serverapp , notebook_path )
4475
45- file_id = file_id_manager .get_id (notebook_path )
46-
47- # Try to get YDoc
48- ydoc = await get_jupyter_ydoc (serverapp , file_id )
49-
50- if ydoc :
76+ if nb :
5177 # Notebook is open in collaborative mode, use YDoc
52- total_cells = len (ydoc .ycells )
53- actual_index = cell_index if cell_index != - 1 else total_cells
54-
55- if actual_index < 0 or actual_index > total_cells :
56- raise ValueError (
57- f"Cell index { cell_index } is out of range. Notebook has { total_cells } cells. Use -1 to append at end."
58- )
59-
60- # Create the cell
61- cell = {
62- "cell_type" : cell_type ,
63- "source" : "" ,
64- }
65- ycell = ydoc .create_ycell (cell )
78+ total_cells = len (nb )
6679
67- # Insert at the specified position
68- if actual_index >= total_cells :
69- ydoc .ycells .append (ycell )
70- else :
71- ydoc .ycells .insert (actual_index , ycell )
80+ # Validate insertion parameters
81+ actual_index = self ._validate_cell_insertion_params (
82+ cell_index , total_cells , cell_type
83+ )
7284
73- # Write content to the cell collaboratively
74- if cell_source :
75- # Set the source directly on the ycell
76- ycell ["source" ] = cell_source
85+ nb .insert_cell (actual_index , cell_source , cell_type )
7786
78- return actual_index , len (ydoc . ycells )
87+ return Notebook ( ** nb . as_dict ()), actual_index , len (nb )
7988 else :
8089 # YDoc not available, use file operations
8190 return await self ._insert_cell_file (notebook_path , cell_index , cell_type , cell_source )
@@ -86,17 +95,21 @@ async def _insert_cell_file(
8695 cell_index : int ,
8796 cell_type : Literal ["code" , "markdown" ],
8897 cell_source : str
89- ) -> tuple [int , int ]:
98+ ) -> tuple [Notebook , int , int ]:
9099 """Insert cell using file operations (non-collaborative mode).
91-
100+
92101 Args:
93102 notebook_path: Absolute path to the notebook
94103 cell_index: Index to insert at (-1 for append)
95- cell_type: Type of cell to insert
104+ cell_type: Type of cell to insert ("code", "markdown")
96105 cell_source: Source content for the cell
97106
98107 Returns:
99- Success message with surrounding cells info
108+ Tuple of (notebook, actual_index, total_cells_after_insertion)
109+
110+ Raises:
111+ IndexError: When cell_index is out of range
112+ ValueError: When cell_type is invalid
100113 """
101114 # Read notebook file
102115 with open (notebook_path , "r" , encoding = "utf-8" ) as f :
@@ -107,55 +120,63 @@ async def _insert_cell_file(
107120 clean_notebook_outputs (notebook )
108121
109122 total_cells = len (notebook .cells )
110- actual_index = cell_index if cell_index != - 1 else total_cells
111123
112- if actual_index < 0 or actual_index > total_cells :
113- raise ValueError (
114- f"Cell index { cell_index } is out of range. Notebook has { total_cells } cells. Use -1 to append at end."
115- )
124+ # Validate insertion parameters
125+ actual_index = self . _validate_cell_insertion_params (
126+ cell_index , total_cells , cell_type
127+ )
116128
117- # Create and insert the cell
129+ # Create and insert the cell using unified method
130+ # Create and insert the cell
118131 if cell_type == "code" :
119132 new_cell = nbformat .v4 .new_code_cell (source = cell_source or "" )
120133 elif cell_type == "markdown" :
121134 new_cell = nbformat .v4 .new_markdown_cell (source = cell_source or "" )
122- else :
123- raise ValueError (f"Invalid cell_type: { cell_type } . Must be 'code' or 'markdown'." )
124-
125135 notebook .cells .insert (actual_index , new_cell )
126136
127137 # Write back to file
128138 with open (notebook_path , "w" , encoding = "utf-8" ) as f :
129139 nbformat .write (notebook , f )
130140
131- return actual_index , len (notebook .cells )
141+ notebook = Notebook (** notebook )
142+
143+ return notebook , actual_index , len (notebook .cells )
132144
133145 async def _insert_cell_websocket (
134146 self ,
135147 notebook_manager : NotebookManager ,
136148 cell_index : int ,
137149 cell_type : Literal ["code" , "markdown" ],
138150 cell_source : str
139- ) -> tuple [int , Notebook ]:
151+ ) -> tuple [Notebook , int , int ]:
140152 """Insert cell using WebSocket connection (MCP_SERVER mode).
141153
142154 Args:
143155 notebook_manager: Notebook manager instance
144156 cell_index: Index to insert at (-1 for append)
145- cell_type: Type of cell to insert
157+ cell_type: Type of cell to insert ("code", "markdown")
146158 cell_source: Source content for the cell
147159
148160 Returns:
149- Success message with surrounding cells info
161+ Tuple of (notebook, actual_index, total_cells_after_insertion)
162+
163+ Raises:
164+ IndexError: When cell_index is out of range
165+ ValueError: When cell_type is invalid
150166 """
151167 async with notebook_manager .get_current_connection () as notebook :
152- actual_index = cell_index if cell_index != - 1 else len (notebook )
153- if actual_index < 0 or actual_index > len (notebook ):
154- raise ValueError (f"Cell index { cell_index } out of range" )
168+ total_cells = len (notebook )
169+
170+ # Validate insertion parameters
171+ actual_index = self ._validate_cell_insertion_params (
172+ cell_index , total_cells , cell_type
173+ )
155174
175+ # Use the unified insert_cell method pattern
176+ # The remote notebook should have: insert_cell(index, source, cell_type)
156177 notebook .insert_cell (actual_index , cell_source , cell_type )
157178
158- return actual_index , Notebook (** notebook .as_dict ())
179+ return Notebook (** notebook .as_dict ()), actual_index , len ( notebook )
159180
160181 async def execute (
161182 self ,
@@ -174,33 +195,44 @@ async def execute(
174195 ) -> str :
175196 """Execute the insert_cell tool.
176197
177- This tool supports three modes of operation:
198+ This tool supports three modes of operation following a unified insertion pattern :
178199
179200 1. JUPYTER_SERVER mode with YDoc (collaborative):
180201 - Checks if notebook is open in a collaborative session
181202 - Uses YDoc for real-time collaborative editing
182203 - Changes are immediately visible to all connected users
204+ - Operations protected by thread locks and YDoc transactions
183205
184206 2. JUPYTER_SERVER mode without YDoc (file-based):
185207 - Falls back to direct file operations using nbformat
186208 - Suitable when notebook is not actively being edited
187209
188210 3. MCP_SERVER mode (WebSocket):
189211 - Uses WebSocket connection to remote Jupyter server
190- - Accesses YDoc through NbModelClient
212+ - Delegates to remote notebook's unified insert_cell method
213+
214+ Thread Safety:
215+ - YDoc mode: Protected by thread lock + YDoc transaction (atomic)
216+ - File mode: No synchronization needed (single-threaded file I/O)
217+ - WebSocket mode: Remote server handles synchronization
191218
192219 Args:
193220 mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
194221 server_client: HTTP client for MCP_SERVER mode
195222 contents_manager: Direct API access for JUPYTER_SERVER mode
196223 notebook_manager: Notebook manager instance
197224 cell_index: Target index for insertion (0-based, -1 to append)
198- cell_type: Type of cell ("code" or "markdown")
225+ cell_type: Type of cell ("code", "markdown")
199226 cell_source: Source content for the cell
200227 **kwargs: Additional parameters
201228
202229 Returns:
203230 Success message with surrounding cells info
231+
232+ Raises:
233+ ValueError: When mode is invalid or required clients are missing
234+ IndexError: When cell_index is out of range
235+ ValueError: When cell_type is invalid
204236 """
205237 if mode == ServerMode .JUPYTER_SERVER and contents_manager is not None :
206238 # JUPYTER_SERVER mode: Try YDoc first, fall back to file operations
@@ -216,29 +248,27 @@ async def execute(
216248 notebook_path = str (Path (root_dir ) / notebook_path )
217249
218250 if serverapp :
219- # Try YDoc approach first
220- actual_index , new_total_cells = await self ._insert_cell_ydoc (serverapp , notebook_path , cell_index , cell_type , cell_source )
251+ # Try YDoc approach first (with thread safety and transactions)
252+ notebook , actual_index , new_total_cells = await self ._insert_cell_ydoc (
253+ serverapp , notebook_path , cell_index , cell_type , cell_source
254+ )
221255 else :
222256 # Fall back to file operations
223- actual_index , new_total_cells = await self ._insert_cell_file (notebook_path , cell_index , cell_type , cell_source )
224-
225- # Load notebook using same API
226- notebook_path = notebook_manager .get_current_notebook_path ()
227- model = await contents_manager .get (notebook_path , content = True , type = 'notebook' )
228- if 'content' not in model :
229- raise ValueError (f"Could not read notebook content from { notebook_path } " )
230- notebook = Notebook (** model ['content' ])
257+ notebook , actual_index , new_total_cells = await self ._insert_cell_file (
258+ notebook_path , cell_index , cell_type , cell_source
259+ )
231260
232261 elif mode == ServerMode .MCP_SERVER and notebook_manager is not None :
233- # MCP_SERVER mode: Use WebSocket connection
234- actual_index , notebook = await self ._insert_cell_websocket (notebook_manager , cell_index , cell_type , cell_source )
235- new_total_cells = len (notebook )
262+ # MCP_SERVER mode: Use WebSocket connection with unified insert_cell pattern
263+ notebook , actual_index , new_total_cells = await self ._insert_cell_websocket (
264+ notebook_manager , cell_index , cell_type , cell_source
265+ )
236266 else :
237267 raise ValueError (f"Invalid mode or missing required clients: mode={ mode } " )
238268
239269 info_list = [f"Cell inserted successfully at index { actual_index } ({ cell_type } )!" ]
240270 info_list .append (f"Notebook now has { new_total_cells } cells, showing surrounding cells:" )
241- # near to end
271+ # Show context near the insertion
242272 if new_total_cells - actual_index < 5 :
243273 start_index = max (0 , new_total_cells - 10 )
244274 else :
0 commit comments