Skip to content

Commit a66904d

Browse files
Prompt/add cite component (#159)
* release v1.0.0 for `general` * add `jupyter-cite` * add docs for prompt section * bump to 0.19.0 * update docs * better docs * Update docs/docs/prompts/index.mdx fix wrong char Co-authored-by: Eric Charles <226720+echarles@users.noreply.github.com> --------- Co-authored-by: Eric Charles <226720+echarles@users.noreply.github.com>
1 parent 859e19d commit a66904d

File tree

11 files changed

+329
-24
lines changed

11 files changed

+329
-24
lines changed

README.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
## 📖 Table of Contents
4848

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

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

68-
## 🔧 Tools Overview
68+
69+
## ✨ MCP Overview
70+
71+
### 🔧 Tools Overview
6972

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

72-
### Server Management Tools
75+
#### Server Management Tools
7376

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

79-
### Multi-Notebook Management Tools
82+
#### Multi-Notebook Management Tools
8083

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

89-
### Cell Operations and Execution Tools
92+
#### Cell Operations and Execution Tools
9093

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

101-
### JupyterLab Integration
104+
#### JupyterLab Integration
102105

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

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

109112
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).
110113

114+
### 📝 Prompt Overview
115+
116+
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.
117+
118+
| Name | Description |
119+
| :------------- | :--------------------------------------------------------------------------------- |
120+
| `jupyter-cite` | Cite specific cells from specified notebook (like `@` in Coding IDE or CLI) |
121+
122+
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).
123+
111124
## 🏁 Getting Started
112125

113126
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.

docs/docs/clients/_category_.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
label: "Clients"
2-
position: 6
2+
position: 7

docs/docs/prompts/_category_.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
label: "Prompts"
2+
position: 6

docs/docs/prompts/index.mdx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Prompts
2+
3+
The server currently offers 1 prompt for user.
4+
5+
:::warning
6+
7+
Not all MCP Clients support the Prompt Feature. You need to ensure that your MCP Client supports it to enable this feature.
8+
Current known MCP Client support status for Prompt:
9+
- 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)
10+
- Not supported: `Cursor`
11+
12+
:::
13+
14+
## Jupyter Core Prompt (1 prompt)
15+
16+
This is the core Prompt component of Jupyter MCP, providing universal and powerful Prompt tools, all prefixed with `jupyter`.
17+
18+
### 1. `jupyter-cite`
19+
20+
This prompt allows users to cite specific cells in a notebook, enabling users to let LLM perform precise subsequent operations on specific cells.
21+
22+
#### Input Parameters
23+
24+
- `--prompt`: User prompt for the cited cells
25+
- `--cell_indices`: Cell indices to cite (0-based), supporting flexible range format
26+
1. **Single Index**: Cite a single cell, such as `"0"` (cites the 1st cell)
27+
2. **Range Format**: Cite a continuous range of cells, such as `"0-2"` (cites cells 1 to 3)
28+
3. **Mixed Format**: Combine single index and range, such as `"0-2,4"` (cites cells 1-3 and cell 5)
29+
4. **Open-ended Range**: From specified index to the end of notebook, such as `"3-"` (cites from cell 4 to the last cell)
30+
- `--notebook_path`: Name of the notebook to cite cells from, default ("") to current activated notebook
31+
32+
#### Output Format
33+
34+
```
35+
USER Cite cells {cell_indices} from notebook {notebook_name}, here are the cells:
36+
=====Cell {cell_index} | type: {cell_type} | execution count: {execution_count}=====
37+
{cell_source}
38+
...(other cells)
39+
=====End of Cited Cells=====
40+
USER's Instruction are follow: {prompt}
41+
```

jupyter_mcp_server/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
"""Jupyter MCP Server."""
66

7-
__version__ = "0.18.2"
7+
__version__ = "0.19.0"

jupyter_mcp_server/server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
ExecuteCodeTool,
5353
ListFilesTool,
5454
ListKernelsTool,
55+
# MCP Prompt
56+
JupyterCitePrompt,
5557
)
5658

5759

@@ -522,6 +524,30 @@ async def execute_code(
522524
max_retries=1
523525
)
524526

527+
###############################################################################
528+
# Prompt
529+
530+
@mcp.prompt()
531+
async def jupyter_cite(
532+
prompt: Annotated[str, Field(description="User prompt for the cited cells")],
533+
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'")],
534+
notebook_name: Annotated[str, Field(description="Name of the notebook to cite cells from, default (empty) to current activated notebook")] = "",
535+
):
536+
"""
537+
Like @ or # in Coding IDE or CLI, cite specific cells from specified notebook and insert them into the prompt.
538+
"""
539+
return await safe_notebook_operation(
540+
lambda: JupyterCitePrompt().execute(
541+
mode=server_context.mode,
542+
server_client=server_context.server_client,
543+
contents_manager=server_context.contents_manager,
544+
notebook_manager=notebook_manager,
545+
cell_indices=cell_indices,
546+
notebook_name=notebook_name,
547+
prompt=prompt,
548+
)
549+
)
550+
525551
###############################################################################
526552
# Helper Functions for Extension.
527553

jupyter_mcp_server/tools/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
from jupyter_mcp_server.tools.list_files_tool import ListFilesTool
3434
from jupyter_mcp_server.tools.list_kernels_tool import ListKernelsTool
3535

36+
# Import MCP prompt
37+
from jupyter_mcp_server.tools.jupyter_cite_prompt import JupyterCitePrompt
38+
3639
__all__ = [
3740
"BaseTool",
3841
"ServerMode",
@@ -54,6 +57,8 @@
5457
"ExecuteCodeTool",
5558
"ListFilesTool",
5659
"ListKernelsTool",
60+
# MCP Prompt
61+
"JupyterCitePrompt",
5762
]
5863

5964

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright (c) 2023-2024 Datalayer, Inc.
2+
#
3+
# BSD 3-Clause License
4+
5+
"""cite from a notebook."""
6+
7+
from typing import Any, Optional
8+
from mcp.server.fastmcp.prompts.base import UserMessage
9+
from jupyter_server_client import JupyterServerClient
10+
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
11+
from jupyter_mcp_server.notebook_manager import NotebookManager
12+
from jupyter_mcp_server.models import Notebook
13+
14+
15+
class JupyterCitePrompt(BaseTool):
16+
"""Tool to cite specific cells from specified notebook."""
17+
18+
def _parse_cell_indices(self, cell_indices_str: str, max_cells: int) -> list[int]:
19+
"""
20+
Parse cell indices from a string with flexible format.
21+
22+
Supports formats like:
23+
- '0,1,2' for individual indices
24+
- '0-2' for ranges
25+
- '0-2,4' for mixed format
26+
- '3-' for from index 3 to end
27+
28+
Args:
29+
cell_indices_str: String with cell indices
30+
max_cells: Maximum number of cells in the notebook
31+
32+
Returns:
33+
List of integer cell indices
34+
35+
Raises:
36+
ValueError: If indices are invalid or out of range
37+
"""
38+
if not cell_indices_str or not cell_indices_str.strip():
39+
raise ValueError("Cell indices cannot be empty")
40+
41+
# Check if notebook is empty
42+
if max_cells <= 0:
43+
raise ValueError("Notebook has no cells")
44+
45+
result = set()
46+
parts = cell_indices_str.split(',')
47+
48+
for part in parts:
49+
part = part.strip()
50+
if not part:
51+
continue
52+
53+
if '-' in part:
54+
# Handle range format
55+
range_parts = part.split('-', 1)
56+
57+
if len(range_parts) == 2:
58+
start_str, end_str = range_parts
59+
60+
if not start_str:
61+
raise ValueError(f"Invalid range format: {part}")
62+
63+
try:
64+
start = int(start_str)
65+
except ValueError:
66+
raise ValueError(f"Invalid start index: {start_str}")
67+
68+
if start < 0:
69+
raise ValueError(f"Start index cannot be negative: {start}")
70+
71+
if not end_str:
72+
# Case: '3-' means from 3 to end
73+
end = max_cells - 1
74+
# Check if start is within range
75+
if start >= max_cells:
76+
raise ValueError(f"Cell index {start} is out of range. Notebook has {max_cells} cells.")
77+
else:
78+
try:
79+
end = int(end_str)
80+
except ValueError:
81+
raise ValueError(f"Invalid end index: {end_str}")
82+
83+
if end < start:
84+
raise ValueError(f"End index ({end}) must be greater than or equal to start index ({start})")
85+
else:
86+
raise ValueError(f"Invalid range format: {part}")
87+
88+
# Add all indices in the range
89+
for i in range(start, end + 1):
90+
if i >= max_cells:
91+
raise ValueError(f"Cell index {i} is out of range. Notebook has {max_cells} cells.")
92+
result.add(i)
93+
else:
94+
# Handle single index
95+
try:
96+
index = int(part)
97+
except ValueError:
98+
raise ValueError(f"Invalid cell index: {part}")
99+
100+
if index < 0:
101+
raise ValueError(f"Cell index cannot be negative: {index}")
102+
if index >= max_cells:
103+
raise ValueError(f"Cell index {index} is out of range. Notebook has {max_cells} cells.")
104+
105+
result.add(index)
106+
107+
# Convert to sorted list
108+
return sorted(result)
109+
110+
async def execute(
111+
self,
112+
mode: ServerMode,
113+
server_client: Optional[JupyterServerClient] = None,
114+
contents_manager: Optional[Any] = None,
115+
notebook_manager: Optional[NotebookManager] = None,
116+
cell_indices: Optional[str] = None,
117+
notebook_name: Optional[str] = None,
118+
prompt: Optional[str] = None,
119+
**kwargs
120+
) -> str:
121+
"""Execute the read_notebook tool.
122+
123+
Args:
124+
mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
125+
contents_manager: Direct API access for JUPYTER_SERVER mode
126+
notebook_manager: Notebook manager instance
127+
notebook_name: Notebook identifier to read
128+
response_format: Response format (brief or detailed)
129+
start_index: Starting index for pagination (0-based)
130+
limit: Maximum number of items to return (0 means no limit)
131+
**kwargs: Additional parameters
132+
133+
Returns:
134+
Formatted table with cell information
135+
"""
136+
if notebook_name == "":
137+
notebook_name = notebook_manager._current_notebook
138+
if notebook_name not in notebook_manager:
139+
raise ValueError(f"Notebook '{notebook_name}' is not connected. All currently connected notebooks: {list(notebook_manager.list_all_notebooks().keys())}")
140+
141+
if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
142+
# Local mode: read notebook directly from file system
143+
notebook_path = notebook_manager.get_notebook_path(notebook_name)
144+
145+
model = await contents_manager.get(notebook_path, content=True, type='notebook')
146+
if 'content' not in model:
147+
raise ValueError(f"Could not read notebook content from {notebook_path}")
148+
notebook = Notebook(**model['content'])
149+
elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
150+
# Remote mode: use WebSocket connection to Y.js document
151+
async with notebook_manager.get_notebook_connection(notebook_name) as notebook_content:
152+
notebook = Notebook(**notebook_content.as_dict())
153+
else:
154+
raise ValueError(f"Invalid mode or missing required clients: mode={mode}")
155+
156+
# Parse cell indices with flexible format
157+
parsed_indices = self._parse_cell_indices(cell_indices, len(notebook))
158+
159+
prompt_list = [f"USER Cite cells {parsed_indices} from notebook {notebook_name}, here are the cells:"]
160+
for cell_index in parsed_indices:
161+
cell = notebook.cells[cell_index]
162+
prompt_list.append(f"=====Cell {cell_index} | type: {cell.cell_type} | execution count: {cell.execution_count if cell.execution_count else 'N/A'}=====")
163+
prompt_list.append(cell.get_source('readable'))
164+
165+
prompt_list.append("=====End of Cited Cells=====")
166+
prompt_list.append(f"USER's Instruction are follow: {prompt}")
167+
168+
return [UserMessage(content="\n".join(prompt_list))]
169+
170+
171+

prompt/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Templates are organized by use case, you can choose any of them.
1414

1515
> [!TIP]
1616
>
17-
> Start with the `general/` template if you're new to Jupyter MCP Server. It provides foundational guidance applicable to most use cases.
17+
> Start with the [`general/`](general/) template if you're new to Jupyter MCP Server. It provides foundational guidance applicable to most use cases.
1818
1919
### Example Usage
2020

0 commit comments

Comments
 (0)