Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
## 📖 Table of Contents

- [Key Features](#-key-features)
- [Tools Overview](#-tools-overview)
- [MCP Overview](#-mcp-overview)
- [Getting Started](#-getting-started)
- [Best Practices](#-best-practices)
- [Contributing](#-contributing)
Expand All @@ -65,18 +65,21 @@

Compatible with any Jupyter deployment (local, JupyterHub, ...) and with [Datalayer](https://datalayer.ai/) hosted Notebooks.

## 🔧 Tools Overview

## ✨ MCP Overview

### 🔧 Tools Overview

The server provides a rich set of tools for interacting with Jupyter notebooks, categorized as follows:

### Server Management Tools
#### Server Management Tools

| Name | Description |
| :--------------- | :----------------------------------------------------------------------------------------- |
| `list_files` | List files and directories in the Jupyter server's file system. |
| `list_kernels` | List all available and running kernel sessions on the Jupyter server. |

### Multi-Notebook Management Tools
#### Multi-Notebook Management Tools

| Name | Description |
| :----------------- | :--------------------------------------------------------------------------------------- |
Expand All @@ -86,7 +89,7 @@ The server provides a rich set of tools for interacting with Jupyter notebooks,
| `unuse_notebook` | Disconnect from a specific notebook and release its resources. |
| `read_notebook` | Read notebook cells source content with brief or detailed format options. |

### Cell Operations and Execution Tools
#### Cell Operations and Execution Tools

| Name | Description |
| :------------------------- | :------------------------------------------------------------------------------- |
Expand All @@ -98,7 +101,7 @@ The server provides a rich set of tools for interacting with Jupyter notebooks,
| `insert_execute_code_cell` | Insert a new code cell and execute it in one step. |
| `execute_code` | Execute code directly in the kernel, supports magic commands and shell commands. |

### JupyterLab Integration
#### JupyterLab Integration

*Available only when JupyterLab mode is enabled. It is enabled by default.*

Expand All @@ -108,6 +111,16 @@ The server provides a rich set of tools for interacting with Jupyter notebooks,

For more details on each tool, their parameters, and return values, please refer to the [official Tools documentation](https://jupyter-mcp-server.datalayer.tech/tools).

### 📝 Prompt Overview

The server also supports [prompt feature](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts) of MCP, providing a easy way for user to interact with Jupyter notebooks.

| Name | Description |
| :------------- | :--------------------------------------------------------------------------------- |
| `jupyter-cite` | Cite specific cells from specified notebook (like `@` in Coding IDE or CLI) |

For more details on each prompt, their input parameters, and return content, please refer to the [official Prompt documentation](https://jupyter-mcp-server.datalayer.tech/prompts).

## 🏁 Getting Started

For comprehensive setup instructions—including `Streamable HTTP` transport, running as a Jupyter Server extension and advanced configuration—check out [our documentation](https://jupyter-mcp-server.datalayer.tech/). Or, get started quickly with `JupyterLab` and `STDIO` transport here below.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/clients/_category_.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
label: "Clients"
position: 6
position: 7
2 changes: 2 additions & 0 deletions docs/docs/prompts/_category_.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: "Prompts"
position: 6
41 changes: 41 additions & 0 deletions docs/docs/prompts/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Prompts

The server currently offers 1 prompt for user.

:::warning

Not all MCP Clients support the Prompt Feature. You need to ensure that your MCP Client supports it to enable this feature.
Current known MCP Client support status for Prompt:
- Supported: `Claude desktop`, [`Claude Code`](https://docs.claude.com/en/docs/claude-code/mcp#execute-mcp-prompts), [`Gemini CLI`](https://geminicli.com/docs/tools/mcp-server/#invoking-prompts)
- Not supported: `Cursor`

:::

## Jupyter Core Prompt (1 prompt)

This is the core Prompt component of Jupyter MCP, providing universal and powerful Prompt tools, all prefixed with `jupyter`.

### 1. `jupyter-cite`

This prompt allows users to cite specific cells in a notebook, enabling users to let LLM perform precise subsequent operations on specific cells.

#### Input Parameters

- `--prompt`: User prompt for the cited cells
- `--cell_indices`: Cell indices to cite (0-based), supporting flexible range format
1. **Single Index**: Cite a single cell, such as `"0"` (cites the 1st cell)
2. **Range Format**: Cite a continuous range of cells, such as `"0-2"` (cites cells 1 to 3)
3. **Mixed Format**: Combine single index and range, such as `"0-2,4"` (cites cells 1-3 and cell 5)
4. **Open-ended Range**: From specified index to the end of notebook, such as `"3-"` (cites from cell 4 to the last cell)
- `--notebook_path`: Name of the notebook to cite cells from, default ("") to current activated notebook

#### Output Format

```
USER Cite cells {cell_indices} from notebook {notebook_name}, here are the cells:
=====Cell {cell_index} | type: {cell_type} | execution count: {execution_count}=====
{cell_source}
...(other cells)
=====End of Cited Cells=====
USER's Instruction are follow: {prompt}
```
2 changes: 1 addition & 1 deletion jupyter_mcp_server/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""Jupyter MCP Server."""

__version__ = "0.18.2"
__version__ = "0.19.0"
26 changes: 26 additions & 0 deletions jupyter_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
ExecuteCodeTool,
ListFilesTool,
ListKernelsTool,
# MCP Prompt
JupyterCitePrompt,
)


Expand Down Expand Up @@ -522,6 +524,30 @@ async def execute_code(
max_retries=1
)

###############################################################################
# Prompt

@mcp.prompt()
async def jupyter_cite(
prompt: Annotated[str, Field(description="User prompt for the cited cells")],
cell_indices: Annotated[str, Field(description="Cell indices to cite (0-based),supporting flexible range format, e.g., '0,1,2', '0-2' or '0-2,4'")],
notebook_name: Annotated[str, Field(description="Name of the notebook to cite cells from, default (empty) to current activated notebook")] = "",
):
"""
Like @ or # in Coding IDE or CLI, cite specific cells from specified notebook and insert them into the prompt.
"""
return await safe_notebook_operation(
lambda: JupyterCitePrompt().execute(
mode=server_context.mode,
server_client=server_context.server_client,
contents_manager=server_context.contents_manager,
notebook_manager=notebook_manager,
cell_indices=cell_indices,
notebook_name=notebook_name,
prompt=prompt,
)
)

###############################################################################
# Helper Functions for Extension.

Expand Down
5 changes: 5 additions & 0 deletions jupyter_mcp_server/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
from jupyter_mcp_server.tools.list_files_tool import ListFilesTool
from jupyter_mcp_server.tools.list_kernels_tool import ListKernelsTool

# Import MCP prompt
from jupyter_mcp_server.tools.jupyter_cite_prompt import JupyterCitePrompt

__all__ = [
"BaseTool",
"ServerMode",
Expand All @@ -54,6 +57,8 @@
"ExecuteCodeTool",
"ListFilesTool",
"ListKernelsTool",
# MCP Prompt
"JupyterCitePrompt",
]


171 changes: 171 additions & 0 deletions jupyter_mcp_server/tools/jupyter_cite_prompt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""cite from a notebook."""

from typing import Any, Optional
from mcp.server.fastmcp.prompts.base import UserMessage
from jupyter_server_client import JupyterServerClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager
from jupyter_mcp_server.models import Notebook


class JupyterCitePrompt(BaseTool):
"""Tool to cite specific cells from specified notebook."""

def _parse_cell_indices(self, cell_indices_str: str, max_cells: int) -> list[int]:
"""
Parse cell indices from a string with flexible format.

Supports formats like:
- '0,1,2' for individual indices
- '0-2' for ranges
- '0-2,4' for mixed format
- '3-' for from index 3 to end

Args:
cell_indices_str: String with cell indices
max_cells: Maximum number of cells in the notebook

Returns:
List of integer cell indices

Raises:
ValueError: If indices are invalid or out of range
"""
if not cell_indices_str or not cell_indices_str.strip():
raise ValueError("Cell indices cannot be empty")

# Check if notebook is empty
if max_cells <= 0:
raise ValueError("Notebook has no cells")

result = set()
parts = cell_indices_str.split(',')

for part in parts:
part = part.strip()
if not part:
continue

if '-' in part:
# Handle range format
range_parts = part.split('-', 1)

if len(range_parts) == 2:
start_str, end_str = range_parts

if not start_str:
raise ValueError(f"Invalid range format: {part}")

try:
start = int(start_str)
except ValueError:
raise ValueError(f"Invalid start index: {start_str}")

if start < 0:
raise ValueError(f"Start index cannot be negative: {start}")

if not end_str:
# Case: '3-' means from 3 to end
end = max_cells - 1
# Check if start is within range
if start >= max_cells:
raise ValueError(f"Cell index {start} is out of range. Notebook has {max_cells} cells.")
else:
try:
end = int(end_str)
except ValueError:
raise ValueError(f"Invalid end index: {end_str}")

if end < start:
raise ValueError(f"End index ({end}) must be greater than or equal to start index ({start})")
else:
raise ValueError(f"Invalid range format: {part}")

# Add all indices in the range
for i in range(start, end + 1):
if i >= max_cells:
raise ValueError(f"Cell index {i} is out of range. Notebook has {max_cells} cells.")
result.add(i)
else:
# Handle single index
try:
index = int(part)
except ValueError:
raise ValueError(f"Invalid cell index: {part}")

if index < 0:
raise ValueError(f"Cell index cannot be negative: {index}")
if index >= max_cells:
raise ValueError(f"Cell index {index} is out of range. Notebook has {max_cells} cells.")

result.add(index)

# Convert to sorted list
return sorted(result)

async def execute(
self,
mode: ServerMode,
server_client: Optional[JupyterServerClient] = None,
contents_manager: Optional[Any] = None,
notebook_manager: Optional[NotebookManager] = None,
cell_indices: Optional[str] = None,
notebook_name: Optional[str] = None,
prompt: Optional[str] = None,
**kwargs
) -> str:
"""Execute the read_notebook tool.

Args:
mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
contents_manager: Direct API access for JUPYTER_SERVER mode
notebook_manager: Notebook manager instance
notebook_name: Notebook identifier to read
response_format: Response format (brief or detailed)
start_index: Starting index for pagination (0-based)
limit: Maximum number of items to return (0 means no limit)
**kwargs: Additional parameters

Returns:
Formatted table with cell information
"""
if notebook_name == "":
notebook_name = notebook_manager._current_notebook
if notebook_name not in notebook_manager:
raise ValueError(f"Notebook '{notebook_name}' is not connected. All currently connected notebooks: {list(notebook_manager.list_all_notebooks().keys())}")

if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
# Local mode: read notebook directly from file system
notebook_path = notebook_manager.get_notebook_path(notebook_name)

model = await contents_manager.get(notebook_path, content=True, type='notebook')
if 'content' not in model:
raise ValueError(f"Could not read notebook content from {notebook_path}")
notebook = Notebook(**model['content'])
elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
# Remote mode: use WebSocket connection to Y.js document
async with notebook_manager.get_notebook_connection(notebook_name) as notebook_content:
notebook = Notebook(**notebook_content.as_dict())
else:
raise ValueError(f"Invalid mode or missing required clients: mode={mode}")

# Parse cell indices with flexible format
parsed_indices = self._parse_cell_indices(cell_indices, len(notebook))

prompt_list = [f"USER Cite cells {parsed_indices} from notebook {notebook_name}, here are the cells:"]
for cell_index in parsed_indices:
cell = notebook.cells[cell_index]
prompt_list.append(f"=====Cell {cell_index} | type: {cell.cell_type} | execution count: {cell.execution_count if cell.execution_count else 'N/A'}=====")
prompt_list.append(cell.get_source('readable'))

prompt_list.append("=====End of Cited Cells=====")
prompt_list.append(f"USER's Instruction are follow: {prompt}")

return [UserMessage(content="\n".join(prompt_list))]



2 changes: 1 addition & 1 deletion prompt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Templates are organized by use case, you can choose any of them.

> [!TIP]
>
> Start with the `general/` template if you're new to Jupyter MCP Server. It provides foundational guidance applicable to most use cases.
> Start with the [`general/`](general/) template if you're new to Jupyter MCP Server. It provides foundational guidance applicable to most use cases.

### Example Usage

Expand Down
Loading
Loading