Skip to content

Commit c5ffffe

Browse files
committed
fix(sse_transport): Resolve connection and routing errors in SSE transport
This commit addresses several critical errors in the SSE transport layer (`sse_transport.py`) that prevented successful client connections after recent refactoring: - Corrected `SseServerTransport` initialization to use the `/messages/` endpoint path, ensuring clients POST back to the correct handler. - Added the necessary Starlette route `Mount` for `/messages` to handle client POSTs. - Fixed the `handle_sse` request handler logic: - Ensured it correctly retrieves streams yielded by `transport.connect_sse`. - Changed the `mcp_server._mcp_server.run()` call to correctly use the underlying `Server` instance (`_mcp_server`) to generate `initialization_options`. - Ensured `run()` is called on the underlying `Server` instance with the required streams and options, resolving previous `TypeError` and `AttributeError` issues. - Removed redundant application setup code within the `run_sse_server` function. These changes ensure the SSE transport now correctly establishes connections, routes client messages, and initiates the MCP session based on the `mcp-sdk`'s intended usage pattern. See CHANGELOG.md for the release summary.
1 parent 22c084d commit c5ffffe

File tree

3 files changed

+125
-63
lines changed

3 files changed

+125
-63
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- Bug in `notebook_read` size estimation loop (`NameError: name 'i' is not defined`).
3131
- Multiple test failures related to incorrect mocking, error expectations, path handling, test setup, and imports (`StringIO`, `FastMCP`).
3232
- Invalid escape sequence in `pyproject.toml` coverage exclusion pattern.
33+
- Several issues in SSE transport (`sse_transport.py`) related to refactoring, including incorrect `SseServerTransport` initialization, missing `/messages` route handling, and incorrect parameters passed to the underlying `mcp.server.Server.run` method, causing connection failures.
3334

3435
## [0.2.2] - 2025-04-19
3536

README.md

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,66 @@ For smooth collaboration with the AI agent on Jupyter Notebooks, you might want
242242
* Ask the user for clarification only if the necessary information cannot be determined after using the investigation tools.
243243
244244
3. **Available Tools:**
245-
* Be aware of the different categories of tools: File operations (`create`, `delete`, `rename`), Notebook/Cell Reading (`read`, `read_cell`, `get_cell_count`, `get_info`), Cell Manipulation (`add_cell`, `edit_cell`, `delete_cell`, `move_cell`, `change_cell_type`, `duplicate_cell`, `split_cell`, `merge_cells`), Metadata (`read/edit_metadata`, `read/edit_cell_metadata`), Outputs (`read_cell_output`, `clear_cell_outputs`, `clear_all_outputs`), and Utility (`
245+
* Be aware of the different categories of tools: File operations (`create`, `delete`, `rename`), Notebook/Cell Reading (`read`, `read_cell`, `get_cell_count`, `get_info`), Cell Manipulation (`add_cell`, `edit_cell`, `delete_cell`, `move_cell`, `change_cell_type`, `duplicate_cell`, `split_cell`, `merge_cells`), Metadata (`read/edit_metadata`, `read/edit_cell_metadata`), Outputs (`read_cell_output`, `clear_cell_outputs`, `clear_all_outputs`), and Utility (`validate`, `export`, `diagnose_imports`).
246+
247+
4. **Math Notation:** For LaTeX in Markdown cells, use `$ ... $` for inline math and `$$ ... $$` for display math. Avoid `\( ... \)` and `\[ ... \]`.
248+
249+
5. **Cell Magics:**
250+
* Avoid unsupported cell magics like `%%bash`, `%%timeit`, and `%%writefile`.
251+
* Use `!command` for shell commands instead of `%%bash`.
252+
* Use `%timeit` (line magic) for timing single statements.
253+
* `%%html` works for rendering HTML output.
254+
* `%%javascript` can execute (e.g., `alert`), but avoid relying on it for manipulating cell output display.
255+
256+
6. **Rich Outputs:** Matplotlib, Pandas DataFrames, Plotly, ipywidgets (`tqdm.notebook`), and embedded HTML in Markdown generally render correctly.
257+
258+
7. **Mermaid:** Diagrams in ` ```mermaid ``` ` blocks are not rendered by default.
259+
260+
8. **Character Escaping in `source` Parameter:**
261+
* When providing the `source` string for `add_cell` or `edit_cell`, ensure that backslashes (`\`) are handled correctly. Newline characters **must** be represented as `\n` (not `\\n`), and LaTeX commands **must** use single backslashes (e.g., `\Sigma`, not `\\Sigma`).
262+
* Incorrect escaping by the tool or its interpretation can break Markdown formatting (like paragraphs intended to be separated by `\n\n`) and LaTeX rendering.
263+
* After adding or editing cells with complex strings (especially those involving newlines or LaTeX), consider using `read_cell` to verify the content was saved exactly as intended and correct if necessary.
246264
```
247265
266+
## Command-Line Arguments
267+
268+
The server accepts the following command-line arguments:
269+
270+
* `--allow-root`: (Required, can use multiple times) Absolute path to directory where notebooks are allowed.
271+
* `--log-dir`: Directory to store log files. Defaults to `~/.cursor_notebook_mcp`.
272+
* `--log-level`: Set the logging level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. Defaults to `INFO`.
273+
* `--max-cell-source-size`: Maximum allowed size in bytes for cell source content. Defaults to 10 MiB.
274+
* `--max-cell-output-size`: Maximum allowed size in bytes for cell output content. Defaults to 10 MiB.
275+
* `--transport`: Transport type to use: `stdio` or `sse`. Defaults to `stdio`.
276+
* `--host`: Host to bind the SSE server to. Only used with `--transport=sse`. Defaults to `127.0.0.1`.
277+
* `--port`: Port to bind the SSE server to. Only used with `--transport=sse`. Defaults to `8080`.
278+
279+
## Security
280+
281+
* **Workspace Root Enforcement:** The server **requires** the `--allow-root` command-line argument during startup. It will refuse to operate on any notebook file located outside the directories specified by these arguments. This is a critical security boundary.
282+
* **Path Handling:** The server uses `os.path.realpath` to resolve paths and checks against the allowed roots before any read or write operation.
283+
* **Input Validation:** Basic checks for `.ipynb` extension are performed.
284+
* **Cell Source Size Limit:** The server enforces a maximum size limit (configurable via `--max-cell-source-size`, default 10 MiB) on the source content provided to `notebook_edit_cell` and `notebook_add_cell` to prevent excessive memory usage.
285+
* **Cell Output Size Limit:** The server enforces a maximum size limit (configurable via `--max-cell-output-size`, default 10 MiB) on the total serialized size of outputs returned by `notebook_read_cell_output`.
286+
287+
## Limitations
288+
289+
* **No Cell Execution:** This server **cannot execute** notebook cells. It operates solely on the `.ipynb` file structure using the `nbformat` library and does not interact with Jupyter kernels. Cell execution must be performed manually by the user within the Cursor UI (selecting the desired kernel and running the cell). Implementing execution capabilities in this server would require kernel management and introduce significant complexity and security considerations.
290+
291+
## Known Issues
292+
293+
* **UI Refresh Issues:** Occasionally, some notebook operations (like cell splitting or merging) may succeed at the file level, but the Cursor UI might not show the updated content correctly. In such situations, you can:
294+
* Close and re-open the notebook file
295+
* Save the file, which might prompt to "Revert" or "Overwrite" - select "Revert" to reload the actual file content
296+
297+
## Development & Testing
298+
299+
1. Setup virtual environment and install dev dependencies:
300+
```bash
301+
python -m venv .venv
302+
source .venv/bin/activate
303+
pip install -e ".[dev]"
304+
```
248305
2. Run tests:
249306
```bash
250307
# Use the wrapper script to ensure environment variables are set
@@ -253,4 +310,45 @@ For smooth collaboration with the AI agent on Jupyter Notebooks, you might want
253310
# ./run_tests.sh tests/test_notebook_tools.py
254311
```
255312
256-
## Issues
313+
## Issues
314+
315+
If you encounter any bugs or issues, please submit them to our GitHub issue tracker:
316+
317+
1. Visit [jbeno/cursor-notebook-mcp](https://github.com/jbeno/cursor-notebook-mcp/issues)
318+
2. Click on "New Issue"
319+
3. Provide:
320+
- A clear description of the problem
321+
- Steps to reproduce the issue
322+
- Expected vs actual behavior
323+
- Your environment details (OS, Python version, etc.)
324+
- Any relevant error messages or logs
325+
- Which model and client/version you're using
326+
327+
## Contributing
328+
329+
Contributions are welcome! Please follow these steps:
330+
331+
1. Fork the repository
332+
2. Create a new branch for your feature (`git checkout -b feature/amazing-feature`)
333+
3. Make your changes
334+
4. Run tests to ensure nothing is broken (`pytest tests/`)
335+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
336+
6. Push to your branch (`git push origin feature/amazing-feature`)
337+
7. Open a Pull Request
338+
339+
Please make sure your PR:
340+
- Includes tests for new functionality
341+
- Updates documentation as needed
342+
- Follows the existing code style
343+
- Includes a clear description of the changes
344+
345+
For major changes, please open an issue first to discuss what you would like to change.
346+
347+
## License
348+
349+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
350+
351+
352+
## Author
353+
354+
This project was created and is maintained by Jim Beno - jim@jimbeno.net

cursor_notebook_mcp/sse_transport.py

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@
4040
async def handle_sse(request):
4141
"""Handles incoming SSE connections and delegates to MCP transport."""
4242
mcp_server = request.app.state.mcp_server
43+
# Retrieve the shared transport instance
44+
transport = request.app.state.sse_transport
4345
config = request.app.state.config
4446
client_host = request.client.host
4547
client_port = request.client.port
4648
log_prefix = f"[SSE {client_host}:{client_port}]"
4749
logger.info(f"{log_prefix} New SSE connection request from {client_host}:{client_port}")
4850

49-
transport = SseServerTransport()
51+
# transport = SseServerTransport(endpoint=mcp_server) # Old incorrect line
5052
try:
5153
logger.debug(f"{log_prefix} Setting up SSE connection")
5254
# connect_sse handles the SSE handshake and provides streams
@@ -55,11 +57,18 @@ async def handle_sse(request):
5557
) as streams:
5658
read_stream, write_stream = streams
5759
logger.info(f"{log_prefix} Connection established. Running MCP session.")
58-
# Run the MCP server logic using the established streams
59-
await mcp_server._mcp_server.run(
60-
transport='stream',
61-
read_stream=read_stream,
62-
write_stream=write_stream
60+
61+
# Get the underlying server instance that has the necessary methods
62+
underlying_server = mcp_server._mcp_server
63+
64+
# Create options using the underlying server instance
65+
init_options = underlying_server.create_initialization_options()
66+
67+
# Call run() on the underlying server instance, passing streams and options
68+
await underlying_server.run(
69+
read_stream=streams[0],
70+
write_stream=streams[1],
71+
initialization_options=init_options
6372
)
6473
logger.info(f"{log_prefix} MCP session finished.")
6574
except Exception as e:
@@ -94,9 +103,14 @@ async def generic_exception_handler(request, exc):
94103
# Function to create the Starlette app (refactored)
95104
def create_starlette_app(mcp_server: FastMCP, config: 'ServerConfig') -> Starlette:
96105
"""Creates the Starlette application instance."""
106+
# Create the SSE transport instance once, passing the **correct** endpoint path
107+
# This is the path the client will be instructed to POST messages back to.
108+
transport = SseServerTransport(endpoint="/messages/")
109+
97110
routes = [
98-
Route("/", endpoint=handle_root, methods=["GET"]),
99-
Route("/sse", endpoint=handle_sse) # Default methods handled by SSE transport
111+
Route("/", endpoint=handle_root, methods=["GET"]), # Root info
112+
Route("/sse", endpoint=handle_sse), # Initial SSE connection (GET)
113+
Mount("/messages", app=transport.handle_post_message) # Client message handler (POST)
100114
]
101115
middleware = [
102116
Middleware(ServerErrorMiddleware, handler=generic_exception_handler)
@@ -111,6 +125,7 @@ def create_starlette_app(mcp_server: FastMCP, config: 'ServerConfig') -> Starlet
111125
# Store shared instances in app state
112126
app.state.mcp_server = mcp_server
113127
app.state.config = config
128+
app.state.sse_transport = transport # Store the transport instance
114129
return app
115130

116131

@@ -140,56 +155,4 @@ def run_sse_server(mcp_server: FastMCP, config: 'ServerConfig'):
140155
raise # Re-raise for main server loop to catch
141156
except Exception as e:
142157
logger.exception(f"Failed to start or run Uvicorn server: {e}")
143-
raise # Re-raise for main server loop to catch
144-
145-
async def health_endpoint(request):
146-
"""Simple health check endpoint."""
147-
return JSONResponse({
148-
"status": "ok",
149-
"service": "Jupyter Notebook MCP Server",
150-
"version": config.version,
151-
"transport": "sse"
152-
})
153-
154-
async def info_endpoint(request):
155-
"""Provides basic server information."""
156-
return JSONResponse({
157-
"name": "Jupyter Notebook MCP Server",
158-
"version": config.version,
159-
"transport": "sse",
160-
"allowed_roots": config.allowed_roots,
161-
# Add other relevant config details if needed
162-
})
163-
164-
# Define Starlette routes
165-
routes = [
166-
Route("/sse", endpoint=handle_sse), # SSE connection endpoint
167-
Route("/health", endpoint=health_endpoint), # Health check
168-
Route("/", endpoint=info_endpoint), # Basic info
169-
Mount("/messages", app=transport.handle_post_message), # Endpoint for clients to POST messages
170-
]
171-
172-
# Create Starlette app
173-
app = Starlette(routes=routes, debug=config.log_level <= logging.DEBUG)
174-
175-
logger.info(f"Starting Uvicorn server on http://{config.host}:{config.port}")
176-
177-
# Configure Uvicorn logging based on server log level
178-
log_config = uvicorn.config.LOGGING_CONFIG
179-
log_config["loggers"]["uvicorn"]["level"] = logging.getLevelName(config.log_level)
180-
log_config["loggers"]["uvicorn.error"]["level"] = logging.getLevelName(config.log_level)
181-
log_config["loggers"]["uvicorn.access"]["level"] = logging.getLevelName(config.log_level)
182-
# Disable access logs propagation if too noisy at DEBUG level
183-
log_config["loggers"]["uvicorn.access"]["propagate"] = config.log_level > logging.DEBUG
184-
185-
try:
186-
# Run the Uvicorn server
187-
uvicorn.run(
188-
app,
189-
host=config.host,
190-
port=config.port,
191-
log_config=log_config
192-
)
193-
except Exception as e:
194-
logger.exception(f"Uvicorn server failed to run: {e}")
195-
raise # Re-raise the exception to be caught by the main loop
158+
raise # Re-raise for main server loop to catch

0 commit comments

Comments
 (0)