This is page 1 of 6. Use http://codebase.md/datalayer/jupyter-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .github │ ├── copilot-instructions.md │ ├── dependabot.yml │ └── workflows │ ├── build.yml │ ├── fix-license-header.yml │ ├── lint.sh │ ├── prep-release.yml │ ├── publish-release.yml │ └── test.yml ├── .gitignore ├── .licenserc.yaml ├── .pre-commit-config.yaml ├── .vscode │ ├── mcp.json │ └── settings.json ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── dev │ ├── content │ │ ├── new.ipynb │ │ ├── notebook.ipynb │ │ └── README.md │ └── README.md ├── Dockerfile ├── docs │ ├── .gitignore │ ├── .yarnrc.yml │ ├── babel.config.js │ ├── docs │ │ ├── _category_.yaml │ │ ├── clients │ │ │ ├── _category_.yaml │ │ │ ├── claude_desktop │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── cline │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── cursor │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ ├── vscode │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ └── windsurf │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── configure │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── contribute │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── deployment │ │ │ ├── _category_.yaml │ │ │ ├── datalayer │ │ │ │ ├── _category_.yaml │ │ │ │ └── streamable-http │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ └── jupyter │ │ │ ├── _category_.yaml │ │ │ ├── index.mdx │ │ │ ├── stdio │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ └── streamable-http │ │ │ ├── _category_.yaml │ │ │ ├── jupyter-extension │ │ │ │ └── index.mdx │ │ │ └── standalone │ │ │ └── index.mdx │ │ ├── index.mdx │ │ ├── releases │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── resources │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ └── tools │ │ ├── _category_.yaml │ │ └── index.mdx │ ├── docusaurus.config.js │ ├── LICENSE │ ├── Makefile │ ├── package.json │ ├── README.md │ ├── sidebars.js │ ├── src │ │ ├── components │ │ │ ├── HomepageFeatures.js │ │ │ ├── HomepageFeatures.module.css │ │ │ ├── HomepageProducts.js │ │ │ └── HomepageProducts.module.css │ │ ├── css │ │ │ └── custom.css │ │ ├── pages │ │ │ ├── index.module.css │ │ │ ├── markdown-page.md │ │ │ └── testimonials.tsx │ │ └── theme │ │ └── CustomDocItem.tsx │ └── static │ └── img │ ├── datalayer │ │ ├── logo.png │ │ └── logo.svg │ ├── favicon.ico │ ├── feature_1.svg │ ├── feature_2.svg │ ├── feature_3.svg │ ├── product_1.svg │ ├── product_2.svg │ └── product_3.svg ├── examples │ └── integration_example.py ├── jupyter_mcp_server │ ├── __init__.py │ ├── __main__.py │ ├── __version__.py │ ├── config.py │ ├── enroll.py │ ├── env.py │ ├── jupyter_extension │ │ ├── __init__.py │ │ ├── backends │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── local_backend.py │ │ │ └── remote_backend.py │ │ ├── context.py │ │ ├── extension.py │ │ ├── handlers.py │ │ └── protocol │ │ ├── __init__.py │ │ └── messages.py │ ├── models.py │ ├── notebook_manager.py │ ├── server_modes.py │ ├── server.py │ ├── tools │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _registry.py │ │ ├── assign_kernel_to_notebook_tool.py │ │ ├── delete_cell_tool.py │ │ ├── execute_cell_tool.py │ │ ├── execute_ipython_tool.py │ │ ├── insert_cell_tool.py │ │ ├── insert_execute_code_cell_tool.py │ │ ├── list_cells_tool.py │ │ ├── list_files_tool.py │ │ ├── list_kernels_tool.py │ │ ├── list_notebooks_tool.py │ │ ├── overwrite_cell_source_tool.py │ │ ├── read_cell_tool.py │ │ ├── read_cells_tool.py │ │ ├── restart_notebook_tool.py │ │ ├── unuse_notebook_tool.py │ │ └── use_notebook_tool.py │ └── utils.py ├── jupyter-config │ ├── jupyter_notebook_config │ │ └── jupyter_mcp_server.json │ └── jupyter_server_config.d │ └── jupyter_mcp_server.json ├── LICENSE ├── Makefile ├── pyproject.toml ├── pytest.ini ├── README.md ├── RELEASE.md ├── smithery.yaml └── tests ├── __init__.py ├── conftest.py ├── test_common.py ├── test_config.py ├── test_jupyter_extension.py ├── test_list_kernels.py ├── test_tools.py └── test_use_notebook.py ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | .Python 6 | env 7 | pip-log.txt 8 | pip-delete-this-directory.txt 9 | .tox 10 | .coverage 11 | .coverage.* 12 | .cache 13 | nosetests.xml 14 | coverage.xml 15 | *.cover 16 | *.log 17 | .git 18 | .github 19 | .mypy_cache 20 | .pytest_cache 21 | dev 22 | docs ``` -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | *.lock 23 | ``` -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- ```yaml 1 | header: 2 | license: 3 | content: | 4 | Copyright (c) 2023-2024 Datalayer, Inc. 5 | 6 | BSD 3-Clause License 7 | 8 | 9 | paths-ignore: 10 | - '**/*.ipynb' 11 | - '**/*.json' 12 | - '**/*.yaml' 13 | - '**/*.yml' 14 | - '**/.*' 15 | - 'docs/**/*' 16 | - 'LICENSE' 17 | 18 | comment: on-failure ``` -------------------------------------------------------------------------------- /docs/.yarnrc.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Copyright (c) Datalayer, Inc. https://datalayer.io 2 | # Distributed under the terms of the MIT License. 3 | 4 | enableImmutableInstalls: false 5 | enableInlineBuilds: false 6 | enableTelemetry: false 7 | httpTimeout: 60000 8 | nodeLinker: node-modules 9 | npmRegistryServer: "https://registry.yarnpkg.com" 10 | checksumBehavior: update 11 | 12 | # This will fix the build error with @lerna/legacy-package-management 13 | # See https://github.com/lerna/repro/pull/11 14 | packageExtensions: 15 | "@lerna/legacy-package-management@*": 16 | dependencies: 17 | "@lerna/child-process": "*" 18 | "js-yaml": "*" 19 | "rimraf": "*" 20 | peerDependencies: 21 | "nx": "*" ``` -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- ```yaml 1 | ci: 2 | autoupdate_schedule: monthly 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: end-of-file-fixer 9 | - id: check-case-conflict 10 | - id: check-executables-have-shebangs 11 | - id: requirements-txt-fixer 12 | - id: check-added-large-files 13 | - id: check-case-conflict 14 | - id: check-toml 15 | - id: check-yaml 16 | - id: debug-statements 17 | - id: forbid-new-submodules 18 | - id: check-builtin-literals 19 | - id: trailing-whitespace 20 | 21 | - repo: https://github.com/python-jsonschema/check-jsonschema 22 | rev: 0.29.4 23 | hooks: 24 | - id: check-github-workflows 25 | 26 | - repo: https://github.com/executablebooks/mdformat 27 | rev: 0.7.19 28 | hooks: 29 | - id: mdformat 30 | additional_dependencies: 31 | [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] 32 | 33 | - repo: https://github.com/charliermarsh/ruff-pre-commit 34 | rev: v0.8.0 35 | hooks: 36 | - id: ruff 37 | args: ["--fix"] 38 | - id: ruff-format 39 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | *.egg-info/ 2 | .ipynb_checkpoints 3 | 4 | # Created by https://www.gitignore.io/api/python 5 | # Edit at https://www.gitignore.io/?templates=python 6 | 7 | ### Python ### 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | dist/ 20 | downloads/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | .installed.cfg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Environment variables: 59 | .env 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # pyenv 71 | .python-version 72 | 73 | # celery beat schedule file 74 | celerybeat-schedule 75 | 76 | # SageMath parsed files 77 | *.sage.py 78 | 79 | # Spyder project settings 80 | .spyderproject 81 | .spyproject 82 | 83 | # Rope project settings 84 | .ropeproject 85 | 86 | # Mr Developer 87 | .mr.developer.cfg 88 | .project 89 | .pydevproject 90 | 91 | # mkdocs documentation 92 | /site 93 | 94 | # mypy 95 | .mypy_cache/ 96 | .dmypy.json 97 | dmypy.json 98 | 99 | # ruff 100 | .ruff_cache 101 | 102 | # Pyre type checker 103 | .pyre/ 104 | 105 | # End of https://www.gitignore.io/api/python 106 | 107 | # OSX files 108 | .DS_Store 109 | 110 | # Include 111 | !**/.*ignore 112 | !**/.*rc 113 | !**/.*rc.js 114 | !**/.*rc.json 115 | !**/.*rc.yml 116 | !**/.*config 117 | !*.*rc.json 118 | !.github 119 | !.devcontainer 120 | 121 | untracked_notebooks/* 122 | .jupyter_ystore 123 | .jupyter_ystore.db 124 | docs/.yarn/* 125 | 126 | uv.lock 127 | *-lock.json 128 | ``` -------------------------------------------------------------------------------- /dev/content/README.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | [](https://datalayer.io) 8 | 9 | [](https://github.com/sponsors/datalayer) 10 | ``` -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | [](https://datalayer.io) 8 | 9 | [](https://github.com/sponsors/datalayer) 10 | ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | [](https://datalayer.io) 8 | 9 | [](https://github.com/sponsors/datalayer) 10 | 11 | # Jupyter MCP Server Docs 12 | 13 | > Source code for the [Jupyter MCP Server Documentation](https://datalayer.io), built with [Docusaurus](https://docusaurus.io). 14 | 15 | ```bash 16 | # Install the dependencies. 17 | conda install yarn 18 | yarn 19 | ``` 20 | 21 | ```bash 22 | # Local Development: This command starts a local development server and opens up a browser window. 23 | # Most changes are reflected live without having to restart the server. 24 | npm start 25 | ``` 26 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | [](https://datalayer.io) 8 | 9 | [](https://github.com/sponsors/datalayer) 10 | 11 | 12 | <div align="center"> 13 | 14 | <!-- omit in toc --> 15 | # 🪐✨ Jupyter MCP Server 16 | 17 | **An [MCP](https://modelcontextprotocol.io) service specifically developed for AI to connect and manage Jupyter Notebooks in real-time** 18 | 19 | *Developed by [Datalayer](https://github.com/datalayer)* 20 | 21 | [](https://pypi.org/project/jupyter-mcp-server) 22 | [](https://www.python.org/downloads/) 23 | [](https://hub.docker.com/r/datalayer/jupyter-mcp-server) 24 | [](https://opensource.org/licenses/BSD-3-Clause) 25 | 26 | 27 | <a href="https://mseep.ai/app/datalayer-jupyter-mcp-server"><img src="https://mseep.net/pr/datalayer-jupyter-mcp-server-badge.png" alt="MseeP.ai Security Assessment Badge" width="100"></a> 28 | <a href="https://archestra.ai/mcp-catalog/datalayer__jupyter-mcp-server"><img src="https://archestra.ai/mcp-catalog/api/badge/quality/datalayer/jupyter-mcp-server" alt="Trust Score" width="150"></a> 29 | 30 | 31 | > 🚨 **Latest Release: v0.14.0**: **Multi-notebook support!** You can now seamlessly switch between multiple notebooks in a single session. [📋 Read more in the release notes](https://jupyter-mcp-server.datalayer.tech/releases) 32 | 33 |  34 | 35 | </div> 36 | 37 | ## 📖 Table of Contents 38 | - [Key Features](#-key-features) 39 | - [Tools Overview](#-tools-overview) 40 | - [Getting Started](#-getting-started) 41 | - [Best Practices](#-best-practices) 42 | - [Contributing](#-contributing) 43 | - [Resources](#-resources) 44 | 45 | 46 | ## 🚀 Key Features 47 | 48 | - ⚡ **Real-time control:** Instantly view notebook changes as they happen. 49 | - 🔁 **Smart execution:** Automatically adjusts when a cell run fails thanks to cell output feedback. 50 | - 🧠 **Context-aware:** Understands the entire notebook context for more relevant interactions. 51 | - 📊 **Multimodal support:** Support different output types, including images, plots, and text. 52 | - 📚 **Multi-notebook support:** Seamlessly switch between multiple notebooks. 53 | - 🤝 **MCP-compatible:** Works with any MCP client, such as Claude Desktop, Cursor, Windsurf, and more. 54 | 55 | Compatible with any Jupyter deployment (local, JupyterHub, ...) and with [Datalayer](https://datalayer.ai/) hosted Notebooks. 56 | 57 | ## 🔧 Tools Overview 58 | 59 | The server provides a rich set of tools for interacting with Jupyter notebooks, categorized as follows: 60 | 61 | ### Server Management 62 | 63 | | Name | Description | 64 | |:---|:---| 65 | | `list_files` | Recursively list files and directories in the Jupyter server's file system. | 66 | | `list_kernels` | List all available and running kernel sessions on the Jupyter server. | 67 | | `assign_kernel_to_notebook` | Create a Jupyter session to connect a notebook file to a specific kernel. | 68 | 69 | ### Multi-Notebook Management 70 | 71 | | Name | Description | 72 | |:---|:---| 73 | | `use_notebook` | Connect to a notebook file, create a new one, or switch between notebooks. | 74 | | `list_notebooks` | List all notebooks available on the Jupyter server and their status | 75 | | `restart_notebook` | Restart the kernel for a specific managed notebook. | 76 | | `unuse_notebook` | Disconnect from a specific notebook and release its resources. | 77 | 78 | ### Cell Operations and Execution 79 | 80 | | Name | Description | 81 | |:---|:---| 82 | | `list_cells` | List basic information for all cells to provide a quick overview of notebook | 83 | | `read_cell` | Read the full content (source and outputs) of a single cell. | 84 | | `read_cells` | Read the full content of all cells in the notebook. | 85 | | `insert_cell` | Insert a new code or markdown cell at a specified position. | 86 | | `delete_cell` | Delete a cell at a specified index. | 87 | | `overwrite_cell_source` | Overwrite the source code of an existing cell. | 88 | | `execute_cell` | Execute a cell with timeout, it supports multimodal output including images. | 89 | | `insert_execute_code_cell` | A convenient tool to insert a new code cell and execute it in one step. | 90 | | `execute_ipython` | Execute IPython code directly in the kernel, including magic and shell commands. | 91 | 92 | 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). 93 | 94 | ## 🏁 Getting Started 95 | 96 | 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. 97 | 98 | ### 1. Set Up Your Environment 99 | 100 | ```bash 101 | pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel 102 | pip uninstall -y pycrdt datalayer_pycrdt 103 | pip install datalayer_pycrdt==0.12.17 104 | ``` 105 | 106 | ### 2. Start JupyterLab 107 | 108 | ```bash 109 | # Start JupyterLab on port 8888, allowing access from any IP and setting a token 110 | jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0 111 | ``` 112 | 113 | > [!NOTE] 114 | > If you are running notebooks through JupyterHub instead of JupyterLab as above, you should: 115 | > 116 | > - Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment. 117 | > - Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub. 118 | 119 | ### 3. Configure Your Preferred MCP Client 120 | 121 | Next, configure your MCP client to connect to the server. We offer two primary methods—choose the one that best fits your needs: 122 | 123 | - **📦 Using `uvx` (Recommended for Quick Start):** A lightweight and fast method using `uv`. Ideal for local development and first-time users. 124 | - **🐳 Using `Docker` (Recommended for Production):** A containerized approach that ensures a consistent and isolated environment, perfect for production or complex setups. 125 | 126 | <details> 127 | <summary><b>📦 Using uvx (Quick Start)</b></summary> 128 | 129 | First, install `uv`: 130 | ```bash 131 | pip install uv 132 | uv --version 133 | # should be 0.6.14 or higher 134 | ``` 135 | See more details on [uv installation](https://docs.astral.sh/uv/getting-started/installation/). 136 | 137 | Then, configure your client: 138 | ```json 139 | { 140 | "mcpServers": { 141 | "jupyter": { 142 | "command": "uvx", 143 | "args": ["jupyter-mcp-server@latest"], 144 | "env": { 145 | "DOCUMENT_URL": "http://localhost:8888", 146 | "DOCUMENT_TOKEN": "MY_TOKEN", 147 | "DOCUMENT_ID": "notebook.ipynb", 148 | "RUNTIME_URL": "http://localhost:8888", 149 | "RUNTIME_TOKEN": "MY_TOKEN", 150 | "ALLOW_IMG_OUTPUT": "true" 151 | } 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | </details> 158 | 159 | <details> 160 | <summary><b>🐳 Using Docker (Production)</b></summary> 161 | 162 | **On macOS and Windows:** 163 | ```json 164 | { 165 | "mcpServers": { 166 | "jupyter": { 167 | "command": "docker", 168 | "args": [ 169 | "run", "-i", "--rm", 170 | "-e", "DOCUMENT_URL", 171 | "-e", "DOCUMENT_TOKEN", 172 | "-e", "DOCUMENT_ID", 173 | "-e", "RUNTIME_URL", 174 | "-e", "RUNTIME_TOKEN", 175 | "-e", "ALLOW_IMG_OUTPUT", 176 | "datalayer/jupyter-mcp-server:latest" 177 | ], 178 | "env": { 179 | "DOCUMENT_URL": "http://host.docker.internal:8888", 180 | "DOCUMENT_TOKEN": "MY_TOKEN", 181 | "DOCUMENT_ID": "notebook.ipynb", 182 | "RUNTIME_URL": "http://host.docker.internal:8888", 183 | "RUNTIME_TOKEN": "MY_TOKEN", 184 | "ALLOW_IMG_OUTPUT": "true" 185 | } 186 | } 187 | } 188 | } 189 | ``` 190 | 191 | **On Linux:** 192 | ```json 193 | { 194 | "mcpServers": { 195 | "jupyter": { 196 | "command": "docker", 197 | "args": [ 198 | "run", "-i", "--rm", 199 | "-e", "DOCUMENT_URL", 200 | "-e", "DOCUMENT_TOKEN", 201 | "-e", "DOCUMENT_ID", 202 | "-e", "RUNTIME_URL", 203 | "-e", "RUNTIME_TOKEN", 204 | "-e", "ALLOW_IMG_OUTPUT", 205 | "--network=host", 206 | "datalayer/jupyter-mcp-server:latest" 207 | ], 208 | "env": { 209 | "DOCUMENT_URL": "http://localhost:8888", 210 | "DOCUMENT_TOKEN": "MY_TOKEN", 211 | "DOCUMENT_ID": "notebook.ipynb", 212 | "RUNTIME_URL": "http://localhost:8888", 213 | "RUNTIME_TOKEN": "MY_TOKEN", 214 | "ALLOW_IMG_OUTPUT": "true" 215 | } 216 | } 217 | } 218 | } 219 | ``` 220 | 221 | </details> 222 | 223 | > [!TIP] 224 | > 1. Ensure the `port` of the `DOCUMENT_URL` and `RUNTIME_URL` match those used in the `jupyter lab` command. 225 | > 2. In a basic setup, `DOCUMENT_URL` and `RUNTIME_URL` are the same. `DOCUMENT_TOKEN`, and `RUNTIME_TOKEN` are also the same and is actually the Jupyter Token. 226 | > 3. The `DOCUMENT_ID` parameter specifies the path to the notebook you want to connect to. It should be relative to the directory where JupyterLab was started. 227 | > - **Optional:** If you omit `DOCUMENT_ID`, the MCP client can automatically list all available notebooks on the Jupyter server, allowing you to select one interactively via your prompts. 228 | > - **Flexible:** Even if you set `DOCUMENT_ID`, the MCP client can still browse, list, switch to, or even create new notebooks at any time. 229 | 230 | For detailed instructions on configuring various MCP clients—including [Claude Desktop](https://jupyter-mcp-server.datalayer.tech/clients/claude_desktop), [VS Code](https://jupyter-mcp-server.datalayer.tech/clients/vscode), [Cursor](https://jupyter-mcp-server.datalayer.tech/clients/cursor), [Cline](https://jupyter-mcp-server.datalayer.tech/clients/cline), and [Windsurf](https://jupyter-mcp-server.datalayer.tech/clients/windsurf) — see the [Clients documentation](https://jupyter-mcp-server.datalayer.tech/clients). 231 | 232 | ## ✅ Best Practices 233 | 234 | - Interact with LLMs that supports multimodal input (like Gemini 2.5 Pro) to fully utilize advanced multimodal understanding capabilities. 235 | - Use a MCP client that supports returning image data and can parse it (like Cursor, Gemini CLI, etc.), as some clients may not support this feature. 236 | - Break down complex task (like the whole data science workflow) into multiple sub-tasks (like data cleaning, feature engineering, model training, model evaluation, etc.) and execute them step-by-step. 237 | 238 | ## 🤝 Contributing 239 | 240 | We welcome contributions of all kinds! Here are some examples: 241 | 242 | - 🐛 Bug fixes 243 | - 📝 Improvements to existing features 244 | - ✨ New feature development 245 | - 📚 Documentation improvements 246 | 247 | For detailed instructions on how to get started with development and submit your contributions, please see our [**Contributing Guide**](CONTRIBUTING.md). 248 | 249 | ### Our Contributors 250 | 251 | <a href="https://github.com/datalayer/jupyter-mcp-server/graphs/contributors"> 252 | <img src="https://contrib.rocks/image?repo=datalayer/jupyter-mcp-server" /> 253 | </a> 254 | 255 | ## 📚 Resources 256 | 257 | Looking for blog posts, videos, or other materials about Jupyter MCP Server? 258 | 259 | 👉 Visit the [**Resources section**](https://jupyter-mcp-server.datalayer.tech/resources) in our documentation for more! 260 | 261 | <a href="https://star-history.com/#/repos/datalayer/jupyter-mcp-server&type=Date"> 262 | <img src="https://api.star-history.com/svg?repos=datalayer/jupyter-mcp-server&type=Date" alt="Star History Chart"> 263 | </a> 264 | 265 | --- 266 | 267 | <div align="center"> 268 | 269 | **If this project is helpful to you, please give us a ⭐️** 270 | 271 | Made with ❤️ by [Datalayer](https://github.com/datalayer) 272 | 273 | </div> ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | # Contributor Covenant Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | We as members, contributors, and leaders pledge to make participation in our 12 | community a harassment-free experience for everyone, regardless of age, body 13 | size, visible or invisible disability, ethnicity, sex characteristics, gender 14 | identity and expression, level of experience, education, socio-economic status, 15 | nationality, personal appearance, race, religion, or sexual identity 16 | and orientation. 17 | 18 | We pledge to act and interact in ways that contribute to an open, welcoming, 19 | diverse, inclusive, and healthy community. 20 | 21 | ## Our Standards 22 | 23 | Examples of behavior that contributes to a positive environment for our 24 | community include: 25 | 26 | * Demonstrating empathy and kindness toward other people 27 | * Being respectful of differing opinions, viewpoints, and experiences 28 | * Giving and gracefully accepting constructive feedback 29 | * Accepting responsibility and apologizing to those affected by our mistakes, 30 | and learning from the experience 31 | * Focusing on what is best not just for us as individuals, but for the 32 | overall community 33 | 34 | Examples of unacceptable behavior include: 35 | 36 | * The use of sexualized language or imagery, and sexual attention or 37 | advances of any kind 38 | * Trolling, insulting or derogatory comments, and personal or political attacks 39 | * Public or private harassment 40 | * Publishing others' private information, such as a physical or email 41 | address, without their explicit permission 42 | * Other conduct which could reasonably be considered inappropriate in a 43 | professional setting 44 | 45 | ## Enforcement Responsibilities 46 | 47 | Community leaders are responsible for clarifying and enforcing our standards of 48 | acceptable behavior and will take appropriate and fair corrective action in 49 | response to any behavior that they deem inappropriate, threatening, offensive, 50 | or harmful. 51 | 52 | Community leaders have the right and responsibility to remove, edit, or reject 53 | comments, commits, code, wiki edits, issues, and other contributions that are 54 | not aligned to this Code of Conduct, and will communicate reasons for moderation 55 | decisions when appropriate. 56 | 57 | ## Scope 58 | 59 | This Code of Conduct applies within all community spaces, and also applies when 60 | an individual is officially representing the community in public spaces. 61 | Examples of representing our community include using an official e-mail address, 62 | posting via an official social media account, or acting as an appointed 63 | representative at an online or offline event. 64 | 65 | ## Enforcement 66 | 67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 68 | reported to the community leaders responsible for enforcement. 69 | All complaints will be reviewed and investigated promptly and fairly. 70 | 71 | All community leaders are obligated to respect the privacy and security of the 72 | reporter of any incident. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 77 | version 2.0, available at 78 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 79 | 80 | [homepage]: https://www.contributor-covenant.org 81 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 82 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | # Contributing to Jupyter MCP Server 8 | 9 | First off, thank you for considering contributing to Jupyter MCP Server! It's people like you that make this project great. Your contributions help us improve the project and make it more useful for everyone! 10 | 11 | ## Code of Conduct 12 | 13 | This project and everyone participating in it is governed by the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior. 14 | 15 | ## How Can I Contribute? 16 | 17 | We welcome contributions of all kinds, including: 18 | - 🐛 Bug fixes 19 | - 📝 Improvements to existing features or documentation 20 | - ✨ New feature development 21 | 22 | ### Reporting Bugs or Suggesting Enhancements 23 | 24 | Before creating a new issue, please **ensure one does not already exist** by searching on GitHub under [Issues](https://github.com/datalayer/jupyter-mcp-server/issues). 25 | 26 | - If you're reporting a bug, please include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 27 | - If you're suggesting an enhancement, clearly state the enhancement you are proposing and why it would be a good addition to the project. 28 | 29 | ## Development Setup 30 | 31 | To get started with development, you'll need to set up your environment. 32 | 33 | 1. **Clone the repository:** 34 | ```bash 35 | git clone https://github.com/datalayer/jupyter-mcp-server 36 | cd jupyter-mcp-server 37 | ``` 38 | 39 | 2. **Install dependencies:** 40 | ```bash 41 | # Install the project in editable mode with test dependencies 42 | pip install -e ".[test]" 43 | ``` 44 | 45 | 3. **Make Some Amazing Changes!** 46 | ```bash 47 | # Make some amazing changes to the source code! 48 | ``` 49 | 50 | 4. **Run Tests:** 51 | ```bash 52 | make test 53 | ``` 54 | 55 | ## (Optional) Manual Agent Testing 56 | 57 | 1. **Build Python Package:** 58 | ```bash 59 | make build 60 | ``` 61 | 62 | 2. **Set Up Your Environment:** 63 | ```bash 64 | pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel 65 | pip uninstall -y pycrdt datalayer_pycrdt 66 | pip install datalayer_pycrdt==0.12.17 67 | ``` 68 | 69 | 3. **Start Jupyter Server:** 70 | ```bash 71 | jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0 72 | ``` 73 | 74 | 4. **Set Up Your MCP Client:** 75 | We recommend using `uvx` to start the MCP server, first install `uvx` with `pip install uv`. 76 | 77 | ```bash 78 | pip install uv 79 | uv --version 80 | # should be 0.6.14 or higher 81 | ``` 82 | 83 | Then, set up your MCP client with the following configuration file. 84 | 85 | ```json 86 | { 87 | "mcpServers": { 88 | "Jupyter-MCP": { 89 | "command": "uvx", 90 | "args": [ 91 | "--from", 92 | "your/path/to/jupyter-mcp-server/dist/jupyter_mcp_server-x.x.x-py3-none-any.whl", 93 | "jupyter-mcp-server" 94 | ], 95 | "env": { 96 | "DOCUMENT_URL": "http://localhost:8888", 97 | "DOCUMENT_TOKEN": "MY_TOKEN", 98 | "DOCUMENT_ID": "notebook.ipynb", 99 | "RUNTIME_URL": "http://localhost:8888", 100 | "RUNTIME_TOKEN": "MY_TOKEN", 101 | "ALLOW_IMG_OUTPUT": "true" 102 | } 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | 5. **Test Your Changes:** 109 | 110 | You Can Test Your Changes with your favorite MCP client(e.g. Cursor, Gemini CLI, etc.). 111 | 112 | ## Pull Request Process 113 | 114 | 1. Once you are satisfied with your changes and tests, commit your code. 115 | 2. Push your branch to your fork and attach with detailed description of the changes you made. 116 | 3. Open a pull request to the `main` branch of the original repository. 117 | 118 | We look forward to your contributions! 119 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/cline/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Cline" 2 | position: 4 3 | ``` -------------------------------------------------------------------------------- /docs/docs/tools/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Tools" 2 | position: 8 3 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/cursor/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Cursor" 2 | position: 3 3 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Clients" 2 | position: 4 3 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/vscode/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "VS Code" 2 | position: 2 3 | ``` -------------------------------------------------------------------------------- /docs/docs/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Overview" 2 | position: 1 3 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/windsurf/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Windsurf" 2 | position: 5 3 | ``` -------------------------------------------------------------------------------- /docs/docs/configure/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Configure" 2 | position: 5 3 | ``` -------------------------------------------------------------------------------- /docs/docs/releases/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Releases" 2 | position: 11 3 | ``` -------------------------------------------------------------------------------- /docs/docs/contribute/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Contribute" 2 | position: 9 3 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Deployment" 2 | position: 2 3 | ``` -------------------------------------------------------------------------------- /docs/docs/resources/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Resources" 2 | position: 12 3 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/claude_desktop/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Claude Desktop" 2 | position: 1 3 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/stdio/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "STDIO Transport" 2 | position: 1 3 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Jupyter Notebooks" 2 | position: 1 3 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/datalayer/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Datalayer Notebooks" 2 | position: 2 3 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/streamable-http/_category_.yaml: -------------------------------------------------------------------------------- ```yaml 1 | label: "Streamable HTTP Transport" 2 | position: 2 3 | ``` -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/datalayer/streamable-http/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Streamable HTTP Transport 2 | 3 | :::warning 4 | Documentation under construction. 5 | ::: 6 | ``` -------------------------------------------------------------------------------- /jupyter-config/jupyter_notebook_config/jupyter_mcp_server.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "ServerApp": { 3 | "nbserver_extensions": { 4 | "jupyter_mcp_server": true 5 | } 6 | } 7 | } 8 | ``` -------------------------------------------------------------------------------- /jupyter-config/jupyter_server_config.d/jupyter_mcp_server.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyter_mcp_server": true 5 | } 6 | } 7 | } 8 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | # Changelog 8 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/__version__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Jupyter MCP Server.""" 6 | 7 | __version__ = "0.16.0" 8 | ``` -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- ``` 1 | ; Copyright (c) 2023-2024 Datalayer, Inc. 2 | ; 3 | ; BSD 3-Clause License 4 | 5 | [pytest] 6 | addopts = -rqA 7 | log_cli = true 8 | log_level = INFO 9 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/jupyter_extension/protocol/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """MCP Protocol implementation for Jupyter Server extension""" 6 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/jupyter_extension/backends/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Backend implementations for notebook and kernel operations""" 6 | ``` -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | module.exports = { 8 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 9 | }; 10 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/__main__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | from jupyter_mcp_server.server import server 6 | 7 | if __name__ == "__main__": 8 | """Start the Jupyter MCP Server.""" 9 | server() 10 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "python-envs.pythonProjects": [ 3 | { 4 | "path": "", 5 | "envManager": "ms-python.python:conda", 6 | "packageManager": "ms-python.python:conda" 7 | } 8 | ] 9 | } ``` -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | --- 8 | title: Markdown page example 9 | --- 10 | 11 | # Markdown page example 12 | 13 | You don't need React to write simple standalone pages. 14 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Jupyter MCP Server.""" 6 | 7 | from jupyter_mcp_server.jupyter_extension.extension import _jupyter_server_extension_points 8 | 9 | 10 | __all__ = ["_jupyter_server_extension_points"] 11 | ``` -------------------------------------------------------------------------------- /.github/workflows/lint.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/usr/bin/env bash 2 | # Copyright (c) 2023-2024 Datalayer, Inc. 3 | # 4 | # BSD 3-Clause License 5 | 6 | pip install -e ".[lint,typing]" 7 | mypy --install-types --non-interactive . 8 | ruff check . 9 | mdformat --check *.md 10 | pipx run 'validate-pyproject[all]' pyproject.toml 11 | ``` -------------------------------------------------------------------------------- /docs/src/components/HomepageProducts.module.css: -------------------------------------------------------------------------------- ```css 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | /* stylelint-disable docusaurus/copyright-header */ 8 | 9 | .product { 10 | display: flex; 11 | align-items: center; 12 | padding: 2rem 0; 13 | width: 100%; 14 | } 15 | 16 | .productSvg { 17 | height: 200px; 18 | width: 200px; 19 | } 20 | ``` -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- ```css 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | /* stylelint-disable docusaurus/copyright-header */ 8 | 9 | .features { 10 | display: flex; 11 | align-items: center; 12 | padding: 2rem 0; 13 | width: 100%; 14 | } 15 | 16 | .featureSvg { 17 | height: 200px; 18 | width: 200px; 19 | } 20 | ``` -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: "pip" 12 | directory: "/" 13 | schedule: 14 | interval: "monthly" 15 | groups: 16 | pip: 17 | patterns: 18 | - "*" 19 | ``` -------------------------------------------------------------------------------- /docs/src/theme/CustomDocItem.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from "react"; 2 | import { ThemeProvider } from '@primer/react-brand'; 3 | import DocItem from "@theme/DocItem"; 4 | 5 | import '@primer/react-brand/lib/css/main.css' 6 | 7 | export const CustomDocItem = (props: any) => { 8 | return ( 9 | <> 10 | <ThemeProvider> 11 | <DocItem {...props}/> 12 | </ThemeProvider> 13 | </> 14 | ) 15 | } 16 | 17 | export default CustomDocItem; 18 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | FROM python:3.10-slim 6 | 7 | WORKDIR /app 8 | 9 | COPY pyproject.toml LICENSE README.md ./ 10 | COPY jupyter_mcp_server/ jupyter_mcp_server/ 11 | COPY jupyter-config/ jupyter-config/ 12 | 13 | RUN pip install --no-cache-dir -e . && \ 14 | pip uninstall -y pycrdt datalayer_pycrdt && \ 15 | pip install --no-cache-dir datalayer_pycrdt==0.12.17 16 | 17 | EXPOSE 4040 18 | 19 | ENTRYPOINT ["python", "-m", "jupyter_mcp_server"] 20 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/jupyter_extension/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """ 6 | Jupyter to MCP Adapter Package 7 | 8 | This package provides the adapter layer to expose MCP server tools as a Jupyter Server extension. 9 | It supports dual-mode operation: standalone MCP server and embedded Jupyter server extension. 10 | """ 11 | 12 | from jupyter_mcp_server.jupyter_extension.context import ServerContext, get_server_context 13 | 14 | __all__ = ["ServerContext", "get_server_context"] 15 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/windsurf/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Windsurf 2 | 3 |  4 | 5 | ## Install Windsurf 6 | 7 | Install the Windsurf app from the [Windsurf website](https://windsurf.com/download). 8 | 9 | ## Configure Jupyter MCP Server 10 | 11 | To use Jupyter MCP Server with Windsurf, add the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server) to your `mcp_config.json` file, read more on the [MCP Windsurf documentation website](https://docs.windsurf.com/windsurf/cascade/mcp). 12 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/cursor/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Cursor 2 | 3 |  4 | 5 | ## Install Cursor 6 | 7 | Install the Cursor app from the [Cursor website](https://www.cursor.com/downloads). 8 | 9 | ## Configure Jupyter MCP Server 10 | 11 | To use Jupyter MCP Server with Cursor, add the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server) to your `.cursor/mcp.json` file, read more on the [MCP Cursor documentation website](https://docs.cursor.com/context/model-context-protocol#configuring-mcp-servers). 12 | ``` -------------------------------------------------------------------------------- /docs/src/pages/testimonials.tsx: -------------------------------------------------------------------------------- ```typescript 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | import React from 'react'; 8 | import Layout from '@theme/Layout'; 9 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 10 | import HomepageFeatures from '../components/HomepageFeatures'; 11 | 12 | export default function Home() { 13 | const {siteConfig} = useDocusaurusContext(); 14 | return ( 15 | <Layout 16 | title={`${siteConfig.title}`} 17 | description="Datalayer, cloud native Jupyter"> 18 | <main> 19 | <HomepageFeatures /> 20 | </main> 21 | </Layout> 22 | ); 23 | } 24 | ``` -------------------------------------------------------------------------------- /docs/docs/contribute/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Contribute 2 | 3 | ## Develop 4 | 5 | Clone the repository and install the dependencies. 6 | 7 | ```bash 8 | git clone https://github.com/datalayer/jupyter-mcp-server 9 | cd jupyter-mcp-server 10 | pip install -e . 11 | ``` 12 | 13 | Build the Docker image from source. 14 | 15 | ```bash 16 | make build-docker 17 | ``` 18 | 19 | ## Contribute 20 | 21 | We invite you to contribute by [opening issues](https://github.com/datalayer/jupyter-mcp-server/issues) and submitting [pull requests](https://github.com/datalayer/jupyter-mcp-server/pulls). 22 | 23 | Your contributions help us improve the project and make it more useful for everyone! 24 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/cline/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Cline 2 | 3 |  4 | 5 | ## Install Cline VS Code extension 6 | 7 | Install the Cline VS Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev). 8 | 9 | ## Configure Jupyter MCP Server 10 | 11 | To use Jupyter MCP Server with Cline, add the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server) to your `.cline_mcp_settings.json` file, read more on the [Cline documentation](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev). 12 | ``` -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- ```javascript 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | /** 8 | * Creating a sidebar enables you to: 9 | - create an ordered group of docs 10 | - render a sidebar for each doc of that group 11 | - provide next/previous navigation 12 | 13 | The sidebars can be generated from the filesystem, or explicitly defined here. 14 | 15 | Create as many sidebars as you want. 16 | */ 17 | 18 | // @ts-check 19 | 20 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 21 | const sidebars = { 22 | jupyterMCPServerSidebar: [ 23 | { 24 | type: 'autogenerated', 25 | dirName: '.', 26 | }, 27 | ] 28 | }; 29 | 30 | module.exports = sidebars; 31 | ``` -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- ```css 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | /* stylelint-disable docusaurus/copyright-header */ 8 | 9 | /** 10 | * CSS files with the .module.css suffix will be treated as CSS modules 11 | * and scoped locally. 12 | */ 13 | 14 | .heroBanner { 15 | padding: 4rem 0; 16 | text-align: center; 17 | position: relative; 18 | overflow: hidden; 19 | } 20 | 21 | @media screen and (max-width: 966px) { 22 | .heroBanner { 23 | padding: 2rem; 24 | } 25 | } 26 | 27 | .buttons { 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .tag { 34 | font-size: small; 35 | padding: 4px; 36 | border-radius: 5px; 37 | border-width: thick; 38 | border-color: red; 39 | background: orange; 40 | } 41 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Clients 2 | 3 | We have tested and validated the Jupyter MCP Server with the following clients: 4 | 5 | - [Claude Desktop](./claude_desktop) 6 | - [VS Code](./vscode) 7 | - [Cursor](./cursor) 8 | - [Cline](./cline) 9 | - [Windsurf](./windsurf) 10 | 11 | The Jupyter MCP Server is also compatible with **ANY** MCP client — see the growing list in [MCP clients](https://modelcontextprotocol.io/clients). This means that you are **NOT** limited to the clients listed above. Both [STDIO](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) and [streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) transports are supported. 12 | 13 | If you prefer a CLI approach as client, you can use for example the python [mcp-client-cli](https://github.com/adhikasp/mcp-client-cli) package. 14 | ``` -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | defaults: 10 | run: 11 | shell: bash -eux {0} 12 | 13 | jobs: 14 | test: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | python-version: ["3.10", "3.13"] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v5 25 | 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v6 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | 31 | - name: Install the extension 32 | run: | 33 | python -m pip install ".[test]" 34 | pip uninstall -y pycrdt datalayer_pycrdt 35 | pip install datalayer_pycrdt==0.12.17 36 | 37 | - name: Test the extension 38 | run: | 39 | make test-mcp-server 40 | make test-jupyter-server 41 | ``` -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "servers": { 3 | // https://github.com/github/github-mcp-server 4 | "Github": { 5 | "url": "https://api.githubcopilot.com/mcp" 6 | }, 7 | // This configuration is for Docker on Linux, read https://jupyter-mcp-server.datalayer.tech/clients/ 8 | "DatalayerJupyter": { 9 | "command": "docker", 10 | "args": [ 11 | "run", 12 | "-i", 13 | "--rm", 14 | "-e", 15 | "DOCUMENT_URL", 16 | "-e", 17 | "DOCUMENT_TOKEN", 18 | "-e", 19 | "DOCUMENT_ID", 20 | "-e", 21 | "RUNTIME_URL", 22 | "-e", 23 | "RUNTIME_TOKEN", 24 | "datalayer/jupyter-mcp-server:latest" 25 | ], 26 | "env": { 27 | "DOCUMENT_URL": "http://host.docker.internal:8888", 28 | "DOCUMENT_TOKEN": "MY_TOKEN", 29 | "DOCUMENT_ID": "notebook.ipynb", 30 | "RUNTIME_URL": "http://host.docker.internal:8888", 31 | "RUNTIME_TOKEN": "MY_TOKEN" 32 | } 33 | } 34 | } 35 | } 36 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - serverUrl 10 | - token 11 | - notebookPath 12 | properties: 13 | serverUrl: 14 | type: string 15 | description: The URL of the JupyterLab server that the MCP will connect to. 16 | token: 17 | type: string 18 | description: The token for authenticating with the JupyterLab server. 19 | notebookPath: 20 | type: string 21 | description: The path to the Jupyter notebook to work with. 22 | commandFunction: 23 | # A function that produces the CLI command to start the MCP on stdio. 24 | |- 25 | (config) => ({ command: 'docker', args: ['run', '-i', '--rm', '-e', `DOCUMENT_URL=${config.serverUrl}`, '-e', `TOKEN=${config.token}`, '-e', `DOCUMENT_ID=${config.notebookPath}`, 'datalayer/jupyter-mcp-server:latest'] }) 26 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | 2 | # Jupyter Notebooks 3 | 4 | This guide will help you set up a Jupyter MCP Server to connect your preferred MCP client to a JupyterLab instance. 5 | The Jupyter MCP Server acts as a bridge between the MCP client and the JupyterLab server, allowing you to interact with Jupyter notebooks seamlessly. 6 | 7 | You can customize the setup further based on your requirements. Refer to the [server configuration](/configure) for more details on the possible configurations. 8 | 9 | Jupyter MCP Server supports two types of transport to connect to your MCP client: **STDIO** and **Streamable HTTP**. Choose the one that best fits your needs. 10 | For more details on the different transports, refer to the official MCP documentation [here](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports). 11 | 12 | If you choose Streamable HTTP transport, you can also choose to run the MCP server **as a Jupyter Server Extension** or **as a Standalone Server**. Running the MCP server as a Jupyter Server Extension has the advantage of not requiring to run two separate servers (Jupyter server + MCP server). 13 | ``` -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@datalayer/jupyter-mcp-server-docs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@datalayer/icons-react": "^1.0.0", 18 | "@datalayer/primer-addons": "^1.0.3", 19 | "@docusaurus/core": "^3.5.2", 20 | "@docusaurus/preset-classic": "^3.5.2", 21 | "@docusaurus/theme-live-codeblock": "^3.5.2", 22 | "@docusaurus/theme-mermaid": "^3.5.2", 23 | "@mdx-js/react": "^3.0.1", 24 | "@primer/react-brand": "^0.58.0", 25 | "clsx": "^2.1.1", 26 | "docusaurus-lunr-search": "^3.5.0", 27 | "react": "18.3.1", 28 | "react-calendly": "^4.1.0", 29 | "react-dom": "18.3.1", 30 | "react-modal-image": "^2.6.0" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/claude_desktop/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Claude Desktop 2 | 3 |  4 | 5 | ## Install Claude Desktop 6 | 7 | Claude Desktop can be downloaded [from this page](https://claude.ai/download) for macOS and Windows. 8 | 9 | For Linux, we had success using this [UNOFFICIAL build script based on nix](https://github.com/k3d3/claude-desktop-linux-flake) 10 | 11 | ```bash 12 | # ⚠️ UNOFFICIAL 13 | # You can also run `make claude-linux` 14 | NIXPKGS_ALLOW_UNFREE=1 nix run github:k3d3/claude-desktop-linux-flake \ 15 | --impure \ 16 | --extra-experimental-features flakes \ 17 | --extra-experimental-features nix-command 18 | ``` 19 | 20 | ## Configure Jupyter MCP Server 21 | 22 | To use Jupyter MCP Server with Claude Desktop, add the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server) to your `claude_desktop_config.json` file, read more on the [MCP documentation website](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server). 23 | 24 | **📺 Watch the setup demo** 25 | 26 | <iframe width="560" height="315" src="https://www.youtube.com/embed/nPllCQxtaxQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen /> 27 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/env.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """ 6 | Environment Configuration Management Module 7 | 8 | This module manages environment variables for multimodal output support. 9 | Following the same pattern as other environment variables in the project. 10 | """ 11 | 12 | import os 13 | 14 | 15 | def _get_env_bool(env_name: str, default_value: bool = True) -> bool: 16 | """ 17 | Get boolean value from environment variable, supporting multiple formats. 18 | 19 | Args: 20 | env_name: Environment variable name 21 | default_value: Default value 22 | 23 | Returns: 24 | bool: Boolean value 25 | """ 26 | env_value = os.getenv(env_name) 27 | if env_value is None: 28 | return default_value 29 | 30 | # Supported true value formats 31 | true_values = {'true', '1', 'yes', 'on', 'enable', 'enabled'} 32 | # Supported false value formats 33 | false_values = {'false', '0', 'no', 'off', 'disable', 'disabled'} 34 | 35 | env_value_lower = env_value.lower().strip() 36 | 37 | if env_value_lower in true_values: 38 | return True 39 | elif env_value_lower in false_values: 40 | return False 41 | else: 42 | return default_value 43 | 44 | 45 | # Multimodal Output Configuration 46 | # Environment variable controls whether to return actual image content or text placeholder 47 | ALLOW_IMG_OUTPUT: bool = _get_env_bool("ALLOW_IMG_OUTPUT", True) 48 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/models.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | from typing import Optional, Literal, Union 6 | from pydantic import BaseModel 7 | from mcp.types import ImageContent 8 | from jupyter_mcp_server.utils import safe_extract_outputs, normalize_cell_source 9 | 10 | 11 | class DocumentRuntime(BaseModel): 12 | provider: str 13 | document_url: str 14 | document_id: str 15 | document_token: str 16 | runtime_url: str 17 | runtime_id: str 18 | runtime_token: str 19 | 20 | 21 | class CellInfo(BaseModel): 22 | """Notebook cell information as returned by the MCP server""" 23 | 24 | index: int 25 | type: Literal["unknown", "code", "markdown"] 26 | source: list[str] 27 | outputs: Optional[list[Union[str, ImageContent]]] 28 | 29 | @classmethod 30 | def from_cell(cls, cell_index: int, cell: dict): 31 | """Extract cell info (create a CellInfo object) from an index and a Notebook cell""" 32 | outputs = None 33 | type = cell.get("cell_type", "unknown") 34 | if type == "code": 35 | try: 36 | outputs = cell.get("outputs", []) 37 | outputs = safe_extract_outputs(outputs) 38 | except Exception as e: 39 | outputs = [f"[Error reading outputs: {str(e)}]"] 40 | 41 | # Properly normalize the cell source to a list of lines 42 | source = normalize_cell_source(cell.get("source", "")) 43 | 44 | return cls( 45 | index=cell_index, type=type, source=source, outputs=outputs 46 | ) 47 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/streamable-http/jupyter-extension/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # As a Jupyter Server Extension 2 | 3 | ## 1. Start JupyterLab and the MCP Server 4 | 5 | ### Environment setup 6 | 7 | Make sure you have the following packages installed in your environment. The collaboration package is needed as the modifications made on the notebook can be seen thanks to [Jupyter Real Time Collaboration](https://jupyterlab.readthedocs.io/en/stable/user/rtc.html). 8 | 9 | ```bash 10 | pip install "jupyter-mcp-server>=0.15.0" "jupyterlab==4.4.1" "jupyter-collaboration==4.0.2" "ipykernel" 11 | pip uninstall -y pycrdt datalayer_pycrdt 12 | pip install datalayer_pycrdt==0.12.17 13 | ``` 14 | 15 | ### JupyterLab and MCP start 16 | 17 | Then, start JupyterLab with the following command. 18 | 19 | ```bash 20 | jupyter lab \ 21 | --JupyterMCPServerExtensionApp.document_url local \ 22 | --JupyterMCPServerExtensionApp.runtime_url local \ 23 | --JupyterMCPServerExtensionApp.document_id notebook.ipynb \ 24 | --JupyterMCPServerExtensionApp.start_new_runtime True \ 25 | --ServerApp.disable_check_xsrf True \ 26 | --IdentityProvider.token MY_TOKEN \ 27 | --ServerApp.root_dir ./dev/content \ 28 | --port 4040 29 | ``` 30 | 31 | You can also run `start-jupyter-server-extension` if you cloned the repository. 32 | 33 | This will start JupyterLab at [http://127.0.0.1:4040](http://127.0.0.1:4040) and the MCP server will be started in the same process. 34 | 35 | ## 2. Configure your MCP Client 36 | 37 | Use the following configuration for you MCP client to connect to a running Jupyter MCP Server. 38 | 39 | ```json 40 | { 41 | "mcpServers": { 42 | "jupyter": { 43 | "command": "npx", 44 | "args": ["mcp-remote", "http://127.0.0.1:4040/mcp"] 45 | } 46 | } 47 | } 48 | ``` 49 | ``` -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- ```javascript 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | import React from 'react'; 8 | import clsx from 'clsx'; 9 | import styles from './HomepageFeatures.module.css'; 10 | 11 | const FeatureList = [ 12 | /* 13 | { 14 | title: 'Easy to Use', 15 | Svg: require('../../static/img/feature_1.svg').default, 16 | description: ( 17 | <> 18 | Datalayer was designed from the ground up to be easily installed and 19 | used to get your data analysis up and running quickly. 20 | </> 21 | ), 22 | }, 23 | { 24 | title: 'Focus on What Matters', 25 | Svg: require('../../static/img/feature_2.svg').default, 26 | description: ( 27 | <> 28 | Datalayer lets you focus on your work, and we'll do the chores. 29 | </> 30 | ), 31 | }, 32 | { 33 | title: 'Powered by Open Source', 34 | Svg: require('../../static/img/feature_3.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your platform to your needs. 38 | </> 39 | ), 40 | }, 41 | */ 42 | ]; 43 | 44 | function Feature({Svg, title, description}) { 45 | return ( 46 | <div className={clsx('col col--4')}> 47 | <div className="text--center"> 48 | <Svg className={styles.featureSvg} alt={title} /> 49 | </div> 50 | <div className="text--center padding-horiz--md"> 51 | <h3>{title}</h3> 52 | <p>{description}</p> 53 | </div> 54 | </div> 55 | ); 56 | } 57 | 58 | export default function HomepageFeatures() { 59 | return ( 60 | <section className={styles.features}> 61 | <div className="container"> 62 | <div className="row"> 63 | {FeatureList.map((props, idx) => ( 64 | <Feature key={idx} {...props} /> 65 | ))} 66 | </div> 67 | </div> 68 | </section> 69 | ); 70 | } 71 | ``` -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | # silent: 16 | # description: "Set a placeholder in the changelog and don't publish the release." 17 | # required: false 18 | # type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | # silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | branch: ${{ github.event.inputs.branch }} 43 | since: ${{ github.event.inputs.since }} 44 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 45 | 46 | - name: "** Next Step **" 47 | run: | 48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 49 | ``` -------------------------------------------------------------------------------- /.github/workflows/fix-license-header.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Fix License Headers 2 | 3 | on: 4 | pull_request_target: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | header-license-fix: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v5 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Checkout the branch from the PR that triggered the job 25 | run: gh pr checkout ${{ github.event.pull_request.number }} 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Fix License Header 30 | # pin to include https://github.com/apache/skywalking-eyes/pull/168 31 | uses: apache/skywalking-eyes/header@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1 32 | with: 33 | mode: fix 34 | 35 | - name: List files changed 36 | id: files-changed 37 | shell: bash -l {0} 38 | run: | 39 | set -ex 40 | export CHANGES=$(git status --porcelain | tee /tmp/modified.log | wc -l) 41 | cat /tmp/modified.log 42 | 43 | echo "N_CHANGES=${CHANGES}" >> $GITHUB_OUTPUT 44 | 45 | git diff 46 | 47 | - name: Commit any changes 48 | if: steps.files-changed.outputs.N_CHANGES != '0' 49 | shell: bash -l {0} 50 | run: | 51 | git config user.name "github-actions[bot]" 52 | git config user.email "github-actions[bot]@users.noreply.github.com" 53 | 54 | git pull --no-tags 55 | 56 | git add * 57 | git commit -m "Automatic application of license header" 58 | 59 | git config push.default upstream 60 | git push 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | ``` -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023-2024 Datalayer, Inc. 3 | # 4 | # BSD 3-Clause License 5 | 6 | """ 7 | Simple test script to verify the configuration system works correctly. 8 | """ 9 | 10 | from jupyter_mcp_server.config import get_config, set_config, reset_config 11 | 12 | def test_config(): 13 | """Test the configuration singleton.""" 14 | print("Testing Jupyter MCP Configuration System") 15 | print("=" * 50) 16 | 17 | # Test default configuration 18 | config = get_config() 19 | print(f"Default runtime_url: {config.runtime_url}") 20 | print(f"Default document_id: {config.document_id}") 21 | print(f"Default provider: {config.provider}") 22 | 23 | # Test setting configuration 24 | new_config = set_config( 25 | runtime_url="http://localhost:9999", 26 | document_id="test_notebooks.ipynb", 27 | provider="datalayer", 28 | runtime_token="test_token" 29 | ) 30 | 31 | print(f"\nUpdated runtime_url: {new_config.runtime_url}") 32 | print(f"Updated document_id: {new_config.document_id}") 33 | print(f"Updated provider: {new_config.provider}") 34 | print(f"Updated runtime_token: {'***' if new_config.runtime_token else 'None'}") 35 | 36 | # Test that singleton works - getting config again should return same values 37 | config2 = get_config() 38 | print(f"\nSingleton test - runtime_url: {config2.runtime_url}") 39 | print(f"Singleton test - document_id: {config2.document_id}") 40 | 41 | # Test reset 42 | reset_config() 43 | config3 = get_config() 44 | print(f"\nAfter reset - runtime_url: {config3.runtime_url}") 45 | print(f"After reset - document_id: {config3.document_id}") 46 | print(f"After reset - provider: {config3.provider}") 47 | 48 | print("\n✅ Configuration system test completed successfully!") 49 | 50 | if __name__ == "__main__": 51 | test_config() 52 | ``` -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 3 | ~ 4 | ~ BSD 3-Clause License 5 | --> 6 | 7 | # Making a new release of jupyter_mcp_server 8 | 9 | The extension can be published to `PyPI` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 10 | 11 | ## Manual release 12 | 13 | ### Python package 14 | 15 | This repository can be distributed as Python 16 | package. All of the Python 17 | packaging instructions in the `pyproject.toml` file to wrap your extension in a 18 | Python package. Before generating a package, we first need to install `build`. 19 | 20 | ```bash 21 | pip install build twine 22 | ``` 23 | 24 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 25 | 26 | ```bash 27 | python -m build 28 | ``` 29 | 30 | Then to upload the package to PyPI, do: 31 | 32 | ```bash 33 | twine upload dist/* 34 | ``` 35 | 36 | ## Automated releases with the Jupyter Releaser 37 | 38 | > [!NOTE] 39 | > The extension repository is compatible with the Jupyter Releaser. But 40 | > the GitHub repository and PyPI may need to be properly set up. Please 41 | > follow the instructions of the Jupyter Releaser [checklist](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html). 42 | 43 | Here is a summary of the steps to cut a new release: 44 | 45 | - Go to the Actions panel 46 | - Run the "Step 1: Prep Release" workflow 47 | - Check the draft changelog 48 | - Run the "Step 2: Publish Release" workflow 49 | 50 | > [!NOTE] 51 | > Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) 52 | > for more information. 53 | 54 | ## Publishing to `conda-forge` 55 | 56 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 57 | 58 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 59 | ``` -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v2 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: "** Next Step **" 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: "** Failure Message **" 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | ``` -------------------------------------------------------------------------------- /docs/docs/releases/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Releases 2 | 3 | ## 0.16.x - 13 Oct 2025 4 | 5 | - [Merge the three execute tools into a single unified tool](https://github.com/datalayer/jupyter-mcp-server/pull/111) 6 | 7 | ## 0.15.x - 08 Oct 2025 8 | 9 | - [Run as Jupyter Server Extension + Tool registry + Use tool](https://github.com/datalayer/jupyter-mcp-server/pull/95) 10 | - [simplify tool implementations](https://github.com/datalayer/jupyter-mcp-server/pull/101) 11 | - [add uvx as alternative MCP server startup method](https://github.com/datalayer/jupyter-mcp-server/pull/101) 12 | - [document as a Jupyter Extension](https://github.com/datalayer/jupyter-mcp-server/pull/101) 13 | - Fix Minor Bugs: [#108](https://github.com/datalayer/jupyter-mcp-server/pull/108),[#110](https://github.com/datalayer/jupyter-mcp-server/pull/110) 14 | 15 | ## 0.14.0 - 03 Oct 2025 16 | 17 | - [Additional Tools & Bug fixes](https://github.com/datalayer/jupyter-mcp-server/pull/93). 18 | - [Execute IPython](https://github.com/datalayer/jupyter-mcp-server/pull/90). 19 | - [Multi notebook management](https://github.com/datalayer/jupyter-mcp-server/pull/88). 20 | 21 | ## 0.13.0 - 25 Sep 2025 22 | 23 | - [Add multimodal output support for Jupyter cell execution](https://github.com/datalayer/jupyter-mcp-server/pull/75). 24 | - [Unify cell insertion functionality](https://github.com/datalayer/jupyter-mcp-server/pull/73). 25 | 26 | ## 0.11.0 - 01 Aug 2025 27 | 28 | - [Rename room to document](https://github.com/datalayer/jupyter-mcp-server/pull/35). 29 | 30 | ## 0.10.2 - 17 Jul 2025 31 | 32 | - [Tools docstring improvements](https://github.com/datalayer/jupyter-mcp-server/pull/30). 33 | 34 | ## 0.10.1 - 11 Jul 2025 35 | 36 | - [CORS Support](https://github.com/datalayer/jupyter-mcp-server/pull/29). 37 | 38 | ## 0.10.0 - 07 Jul 2025 39 | 40 | - More [fixes](https://github.com/datalayer/jupyter-mcp-server/pull/28) issues for nbclient stop. 41 | 42 | ## 0.9.0 - 02 Jul 2025 43 | 44 | - Fix issues with `nbmodel` stops. 45 | 46 | ## 0.6.0 - 01 Jul 2025 47 | 48 | - Configuration change, see details on the [clients page](/clients) and [server configuration](/configure). 49 | ``` -------------------------------------------------------------------------------- /docs/src/components/HomepageProducts.js: -------------------------------------------------------------------------------- ```javascript 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | import React from 'react'; 8 | import clsx from 'clsx'; 9 | import styles from './HomepageProducts.module.css'; 10 | 11 | const ProductList = [ 12 | /* 13 | { 14 | title: 'Jupyter MCP Server', 15 | Svg: require('../../static/img/product_1.svg').default, 16 | description: ( 17 | <> 18 | Get started by creating a Jupyter platform in the cloud with Jupyter MCP Server. You will get Jupyter on Kubernetes with a cloud database and storage bucket to persist your notebooks and datasets. 19 | </> 20 | ), 21 | }, 22 | { 23 | title: 'Jupyter', 24 | Svg: require('../../static/img/product_2.svg').default, 25 | description: ( 26 | <> 27 | If you need more batteries for Jupyter, have a look to our Jupyter components. The components allow you to get the best of Jupyter notebooks, with features like authentication, authorization, React.js user interface, server and kernel instant start, administration... 28 | </> 29 | ), 30 | }, 31 | { 32 | title: 'Sharebook', 33 | Svg: require('../../static/img/product_3.svg').default, 34 | description: ( 35 | <> 36 | For a truly collaborative and accessible notebook, try Sharebook, a better better literate notebook, with built-in collaboration, accessibility... 37 | </> 38 | ), 39 | }, 40 | */ 41 | ]; 42 | 43 | function Product({Svg, title, description}) { 44 | return ( 45 | <div className={clsx('col col--4')}> 46 | <div className="text--center"> 47 | <Svg className={styles.productSvg} alt={title} /> 48 | </div> 49 | <div className="text--center padding-horiz--md"> 50 | <h3>{title}</h3> 51 | <p>{description}</p> 52 | </div> 53 | </div> 54 | ); 55 | } 56 | 57 | export default function HomepageProducts() { 58 | return ( 59 | <section className={styles.Products}> 60 | <div className="container"> 61 | <div className="row"> 62 | {ProductList.map((props, idx) => ( 63 | <Product key={idx} {...props} /> 64 | ))} 65 | </div> 66 | </div> 67 | </section> 68 | ); 69 | } 70 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Deployment 2 | 3 | Jupyter MCP Server can be deployed in various configurations depending on your needs. It can be running inside the Jupyter Server **as a Jupyter Server Extension**, or as a **Standalone Server** connecting to a **local or remote Jupyter server** or to [**Datalayer**](https://datalayer.ai) hosted Notebooks. 4 | 5 | Navigate to the relevant section based on your needs: 6 | - ***Jupyter Notebooks***: If you want to interact with notebooks in JupyterLab/JupyterHub. 7 | - ***Datalayer Notebooks***: If you want to interact with notebooks hosted on [Datalayer](https://datalayer.ai). 8 | - ***STDIO Transport***: If you want to set up the MCP Server using standard input/output (STDIO) transport. 9 | - ***Streamable HTTP Transport***: If you want to set up the MCP Server using Streamable HTTP transport. 10 | - ***As a Standalone Server***: If you want to set up the MCP Server as a Standalone Server. 11 | - ***As a Jupyter Server Extension***: If you want to set up the MCP Server as a Jupyter Server Extension. This has for advantage to avoid running 2 separate servers (Jupyter server + MCP server) but only supports Streamable HTTP transport. 12 | 13 | You can find below diagrams illustrating the different configurations. 14 | 15 | ## As a Standalone Server 16 | 17 | The following diagram illustrates how **Jupyter MCP Server** connects to a **Jupyter server** or **Datalayer** and communicates with an MCP client. 18 | 19 | <img 20 | src="https://assets.datalayer.tech/jupyter-mcp/diagram.png" 21 | alt="Jupyter MCP Diagram" 22 | style={{ width: "700px", marginBottom: "2rem" }} 23 | /> 24 | 25 | ## As a Jupyter Server Extension 26 | 27 | The following diagram illustrates how **Jupyter MCP Server** runs as an extension inside a **Jupyter server** and communicates with an MCP client. 28 | In this configuration, you don't need to run a separate MCP server. It will start automatically when you start your Jupyter server. 29 | Note that only **Streamable HTTP** transport is supported in this configuration. 30 | 31 | <img 32 | src="https://assets.datalayer.tech/jupyter-mcp/diagram-jupyter-extension.png" 33 | alt="Jupyter MCP Diagram Jupyter Extension" 34 | style={{ width: "700px", marginBottom: "2rem" }} 35 | /> 36 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/_base.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Base classes and enums for MCP tools.""" 6 | 7 | from abc import ABC, abstractmethod 8 | from enum import Enum 9 | from typing import Any, Optional 10 | 11 | from jupyter_server_api import JupyterServerClient 12 | from jupyter_kernel_client import KernelClient 13 | 14 | 15 | class ServerMode(str, Enum): 16 | """Enum to indicate which server mode the tool is running in.""" 17 | MCP_SERVER = "mcp_server" 18 | JUPYTER_SERVER = "jupyter_server" 19 | 20 | 21 | class BaseTool(ABC): 22 | """Abstract base class for all MCP tools. 23 | 24 | Each tool must implement the execute method which handles both 25 | MCP_SERVER mode (using HTTP clients) and JUPYTER_SERVER mode 26 | (using direct API access to serverapp managers). 27 | """ 28 | 29 | def __init__(self): 30 | """Initialize the tool.""" 31 | pass 32 | 33 | @abstractmethod 34 | async def execute( 35 | self, 36 | mode: ServerMode, 37 | server_client: Optional[JupyterServerClient] = None, 38 | kernel_client: Optional[KernelClient] = None, 39 | contents_manager: Optional[Any] = None, 40 | kernel_manager: Optional[Any] = None, 41 | kernel_spec_manager: Optional[Any] = None, 42 | **kwargs 43 | ) -> Any: 44 | """Execute the tool logic. 45 | 46 | Args: 47 | mode: ServerMode indicating MCP_SERVER or JUPYTER_SERVER 48 | server_client: JupyterServerClient for HTTP access (MCP_SERVER mode) 49 | kernel_client: KernelClient for kernel HTTP access (MCP_SERVER mode) 50 | contents_manager: Direct access to contents manager (JUPYTER_SERVER mode) 51 | kernel_manager: Direct access to kernel manager (JUPYTER_SERVER mode) 52 | kernel_spec_manager: Direct access to kernel spec manager (JUPYTER_SERVER mode) 53 | **kwargs: Tool-specific parameters 54 | 55 | Returns: 56 | Tool execution result (type varies by tool) 57 | """ 58 | pass 59 | 60 | @property 61 | @abstractmethod 62 | def name(self) -> str: 63 | """Return the tool name.""" 64 | pass 65 | 66 | @property 67 | @abstractmethod 68 | def description(self) -> str: 69 | """Return the tool description.""" 70 | pass 71 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/server_modes.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Utility functions for detecting and handling server mode.""" 6 | 7 | from typing import Tuple, Optional, Any 8 | from jupyter_server_api import JupyterServerClient 9 | from jupyter_mcp_server.config import get_config 10 | 11 | 12 | def get_server_mode_and_clients() -> Tuple[str, Optional[JupyterServerClient], Optional[Any], Optional[Any], Optional[Any]]: 13 | """Determine server mode and get appropriate clients/managers. 14 | 15 | Returns: 16 | Tuple of (mode, server_client, contents_manager, kernel_manager, kernel_spec_manager) 17 | - mode: "local" if using local API, "http" if using HTTP clients 18 | - server_client: JupyterServerClient or None 19 | - contents_manager: Local contents manager or None 20 | - kernel_manager: Local kernel manager or None 21 | - kernel_spec_manager: Local kernel spec manager or None 22 | """ 23 | config = get_config() 24 | 25 | # Check if we should use local API 26 | try: 27 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 28 | context = get_server_context() 29 | 30 | if context.is_local_document() and context.get_contents_manager() is not None: 31 | # JUPYTER_SERVER mode with local API access 32 | return ( 33 | "local", 34 | None, 35 | context.get_contents_manager(), 36 | context.get_kernel_manager(), 37 | context.get_kernel_spec_manager() 38 | ) 39 | except (ImportError, Exception): 40 | # Context not available or error, fall through to HTTP mode 41 | pass 42 | 43 | # MCP_SERVER mode with HTTP clients 44 | server_client = JupyterServerClient( 45 | base_url=config.runtime_url, 46 | token=config.runtime_token 47 | ) 48 | 49 | return ("http", server_client, None, None, None) 50 | 51 | 52 | def is_local_mode() -> bool: 53 | """Check if running in local API mode. 54 | 55 | Returns: 56 | True if using local serverapp API, False if using HTTP clients 57 | """ 58 | try: 59 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 60 | context = get_server_context() 61 | return context.is_local_document() and context.get_contents_manager() is not None 62 | except (ImportError, Exception): 63 | return False 64 | ``` -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | defaults: 9 | run: 10 | shell: bash -eux {0} 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | python-version: ["3.10", "3.13"] 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v5 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Base Setup 31 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 32 | 33 | - name: Install the extension 34 | run: | 35 | python -m pip install ".[test]" 36 | 37 | - name: Build the extension 38 | run: | 39 | pip install build 40 | python -m build --sdist 41 | cp dist/*.tar.gz jupyter_mcp_server.tar.gz 42 | pip uninstall -y "jupyter_mcp_server" 43 | rm -rf "jupyter_mcp_server" 44 | 45 | - uses: actions/upload-artifact@v4 46 | if: startsWith(matrix.os, 'ubuntu') 47 | with: 48 | name: jupyter_mcp_server-sdist-${{ matrix.python-version }} 49 | path: jupyter_mcp_server.tar.gz 50 | 51 | check_links: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v5 55 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 56 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 57 | 58 | test_lint: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v5 62 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 63 | - name: Run Linters 64 | run: | 65 | bash ./.github/workflows/lint.sh 66 | 67 | test_sdist: 68 | needs: build 69 | runs-on: ubuntu-latest 70 | strategy: 71 | matrix: 72 | python-version: ["3.13"] 73 | 74 | steps: 75 | - name: Checkout 76 | uses: actions/checkout@v5 77 | - name: Install Python 78 | uses: actions/setup-python@v6 79 | with: 80 | python-version: ${{ matrix.python-version }} 81 | architecture: "x64" 82 | - uses: actions/download-artifact@v5 83 | with: 84 | name: jupyter_mcp_server-sdist-${{ matrix.python-version }} 85 | - name: Install and Test 86 | run: | 87 | pip install jupyter_mcp_server.tar.gz 88 | pip list 2>&1 | grep -ie "jupyter_mcp_server" 89 | python -c "import jupyter_mcp_server" 90 | ``` -------------------------------------------------------------------------------- /docs/docs/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Overview 3 | sidebar_position: 1 4 | hide_table_of_contents: false 5 | slug: / 6 | --- 7 | 8 | # Overview 9 | 10 | :::info 11 | 12 | **🚨 NEW IN 0.14.0:** Multi-notebook support! 13 | You can now seamlessly switch between multiple notebooks in a single session. 14 | [Read more in the release notes.](https://jupyter-mcp-server.datalayer.tech/releases) 15 | 16 | ::: 17 | 18 | **Jupyter MCP Server** is a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server implementation that enables **real-time** interaction with 📓 Jupyter Notebooks, allowing AI to edit, document and execute code for data analysis, visualization etc. 19 | 20 | Compatible with any Jupyter deployment (local, JupyterHub, ...) and with [Datalayer](https://datalayer.ai) hosted Notebooks. [Open an issue](https://github.com/datalayer/jupyter-mcp-server/issues) to discuss adding your solution as provider. 21 | 22 | Key features include: 23 | 24 | - ⚡ **Real-time control:** Instantly view notebook changes as they happen. 25 | - 🔁 **Smart execution:** Automatically adjusts when a cell run fails thanks to cell output feedback. 26 | - 🧠 **Context-aware:** Understands the entire notebook context for more relevant interactions. 27 | - 📊 **Multimodal support:** Support different output types, including images, plots, and text. 28 | - 📁 **Multi-notebook support:** Seamlessly switch between multiple notebooks. 29 | - 🤝 **MCP-compatible:** Works with any MCP client, such as [Claude Desktop](/clients/claude_desktop), [Cursor](/clients/cursor), [Cline](/clients/cline), [Windsurf](/clients/windsurf) and more. 30 | 31 | To use Jupyter MCP Server, you first need to decide which setup fits your needs: 32 | - ***Editor***: Do you want to interact with notebooks in Jupyter or with Datalayer hosted Notebooks? 33 | - ***MCP Transport***: Do you want to set up the MCP Server using standard input/output (STDIO) transport or Streamable HTTP transport? 34 | - ***MCP Server Location***: Do you want to set up the MCP Server as a Standalone Server or as a Jupyter Server Extension? 35 | 36 | Navigate to the relevant section in the [Deployment](./deployment) page to get started based on your needs. 37 | 38 | Looking for blog posts, videos or other resources related to Jupyter MCP Server? <br /> 39 | 👉 Check out the [Resources](./resources) section. 40 | 41 | 🧰 Dive into the [Tools section](./tools) to understand the tools powering the server. 42 | 43 |  44 | ``` -------------------------------------------------------------------------------- /docs/docs/resources/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # Resources 2 | 3 | ## Articles & Blog Posts 4 | 5 | - [HuggingFace Blog - How to Install and Use Jupyter MCP Server](https://huggingface.co/blog/lynn-mikami/jupyter-mcp-server) 6 | - [Analytics Vidhya - How to Use Jupyter MCP Server?](https://www.analyticsvidhya.com/blog/2025/05/jupyter-mcp-server/) 7 | - [Medium AI Simplified in Plain English - How to Use Jupyter MCP Server?](https://medium.com/ai-simplified-in-plain-english/how-to-use-jupyter-mcp-server-87f68fea7471) 8 | - [Medium Jupyter AI Agents - Jupyter MCP Server: How to Setup via Claude Desktop](https://jupyter-ai-agents.datalayer.blog/mcp-server-for-jupyter-heres-your-guide-2025-0b29d975b4e1) 9 | - [Medium Data Science in Your Pocket - Best MCP Servers for Data Scientists](https://medium.com/data-science-in-your-pocket/best-mcp-servers-for-data-scientists-ee4fa6caf066) 10 | - [Medium Coding Nexus - 6 Open Source MCP Servers Every Dev Should Try](https://medium.com/coding-nexus/6-open-source-mcp-servers-every-dev-should-try-b3cc6cf6a714) 11 | - [Medium Joe Njenga - 8 Best MCP Servers & Tools Every Python Developer Should Try](https://medium.com/@joe.njenga/8-best-mcp-servers-tools-every-python-developer-should-try-3e69f435e99e) 12 | - [Medium Sreekar Kashyap - MCP Servers + Ollama](https://medium.com/@sreekarkashyap7/mcp-servers-ollama-fad991461e88) 13 | - [Medium Wenmin Wu - Agentic DS Workflow with Cursor and MCP Servers](https://medium.com/@wenmin_wu/agentic-ds-workflow-with-cursor-and-mcp-servers-2d90a102cf31) 14 | 15 | ## Videos 16 | 17 | - [Data Science in your pocket - Jupyter MCP : AI for Jupyter Notebooks](https://www.youtube.com/watch?v=qkoEsqiWDOU) 18 | - [Datalayer - How to Set Up the Jupyter MCP Server (via Claude Desktop)](https://www.youtube.com/watch?v=nPllCQxtaxQ) 19 | 20 | ## MCP Directories 21 | 22 | - [Model Context Protocol Servers](https://github.com/modelcontextprotocol/servers) 23 | - [Awesome MCP Servers](https://github.com/punkpeye/awesome-mcp-servers) 24 | 25 | ## MCP Registries 26 | 27 | - [MCP.so](https://mcp.so/server/Integrating-the-Jupyter-server-with-claude-desktop-uisng-the-powerful-model-context-protocol/harshitha-8) 28 | - [MCP Market](https://mcpmarket.com/server/jupyter) 29 | - [MCP Servers Finder](https://www.mcpserverfinder.com/servers/ihrpr/mcp-server-jupyter) 30 | - [Pulse MCP](https://www.pulsemcp.com/servers/datalayer-jupyter) 31 | - [Playbooks](https://playbooks.com/mcp/datalayer-jupyter) 32 | - [Know That AI](https://knowthat.ai/agents/jupyter-server) 33 | 34 | <!-- 35 | - [Smithery](https://smithery.ai/server/@datalayer/jupyter-mcp-server) 36 | --> 37 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Tools package for Jupyter MCP Server. 6 | 7 | Each tool is implemented as a separate class with an execute method 8 | that can operate in either MCP_SERVER or JUPYTER_SERVER mode. 9 | """ 10 | 11 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 12 | from jupyter_mcp_server.tools._registry import ToolRegistry, get_tool_registry, register_tool 13 | 14 | # Import tool implementations - Notebook Management 15 | from jupyter_mcp_server.tools.list_notebooks_tool import ListNotebooksTool 16 | from jupyter_mcp_server.tools.restart_notebook_tool import RestartNotebookTool 17 | from jupyter_mcp_server.tools.unuse_notebook_tool import UnuseNotebookTool 18 | from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool 19 | 20 | # Import tool implementations - Cell Reading 21 | from jupyter_mcp_server.tools.read_cells_tool import ReadCellsTool 22 | from jupyter_mcp_server.tools.list_cells_tool import ListCellsTool 23 | from jupyter_mcp_server.tools.read_cell_tool import ReadCellTool 24 | 25 | # Import tool implementations - Cell Writing 26 | from jupyter_mcp_server.tools.insert_cell_tool import InsertCellTool 27 | from jupyter_mcp_server.tools.insert_execute_code_cell_tool import InsertExecuteCodeCellTool 28 | from jupyter_mcp_server.tools.overwrite_cell_source_tool import OverwriteCellSourceTool 29 | from jupyter_mcp_server.tools.delete_cell_tool import DeleteCellTool 30 | 31 | # Import tool implementations - Cell Execution 32 | from jupyter_mcp_server.tools.execute_cell_tool import ExecuteCellTool 33 | 34 | # Import tool implementations - Other Tools 35 | from jupyter_mcp_server.tools.assign_kernel_to_notebook_tool import AssignKernelToNotebookTool 36 | from jupyter_mcp_server.tools.execute_ipython_tool import ExecuteIpythonTool 37 | from jupyter_mcp_server.tools.list_files_tool import ListFilesTool 38 | from jupyter_mcp_server.tools.list_kernels_tool import ListKernelsTool 39 | 40 | __all__ = [ 41 | "BaseTool", 42 | "ServerMode", 43 | "ToolRegistry", 44 | "get_tool_registry", 45 | "register_tool", 46 | # Notebook Management 47 | "ListNotebooksTool", 48 | "RestartNotebookTool", 49 | "UnuseNotebookTool", 50 | "UseNotebookTool", 51 | # Cell Reading 52 | "ReadCellsTool", 53 | "ListCellsTool", 54 | "ReadCellTool", 55 | # Cell Writing 56 | "InsertCellTool", 57 | "InsertExecuteCodeCellTool", 58 | "OverwriteCellSourceTool", 59 | "DeleteCellTool", 60 | # Cell Execution 61 | "ExecuteCellTool", 62 | # Other Tools 63 | "AssignKernelToNotebookTool", 64 | "ExecuteIpythonTool", 65 | "ListFilesTool", 66 | "ListKernelsTool", 67 | ] 68 | 69 | 70 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | [build-system] 6 | requires = ["hatchling~=1.21"] 7 | build-backend = "hatchling.build" 8 | 9 | [project] 10 | name = "jupyter_mcp_server" 11 | authors = [{ name = "Datalayer", email = "[email protected]" }] 12 | dynamic = ["version"] 13 | readme = "README.md" 14 | requires-python = ">=3.10" 15 | keywords = ["Jupyter"] 16 | classifiers = [ 17 | "Intended Audience :: Developers", 18 | "Intended Audience :: System Administrators", 19 | "License :: OSI Approved :: BSD License", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | ] 23 | dependencies = [ 24 | "jupyter-server-nbmodel", 25 | "jupyter-kernel-client>=0.7.3", 26 | "jupyter-nbmodel-client>=0.14.2", 27 | "jupyter-server-api", 28 | "jupyter_server>=1.6,<3", 29 | "tornado>=6.1", 30 | "traitlets>=5.0", 31 | "mcp[cli]>=1.10.1", 32 | "pydantic", 33 | "uvicorn", 34 | "click", 35 | "fastapi" 36 | ] 37 | 38 | [project.optional-dependencies] 39 | test = [ 40 | "ipykernel", 41 | "jupyter_server>=1.6,<3", 42 | "pytest>=7.0", 43 | "pytest-asyncio", 44 | "pytest-timeout>=2.1.0", 45 | "jupyterlab==4.4.1", 46 | "jupyter-collaboration==4.0.2", 47 | "datalayer_pycrdt==0.12.17", 48 | "pillow>=10.0.0" 49 | ] 50 | lint = ["mdformat>0.7", "mdformat-gfm>=0.3.5", "ruff"] 51 | typing = ["mypy>=0.990"] 52 | 53 | [project.scripts] 54 | jupyter-mcp-server = "jupyter_mcp_server.server:server" 55 | 56 | [project.license] 57 | file = "LICENSE" 58 | 59 | [project.urls] 60 | Home = "https://github.com/datalayer/jupyter-mcp-server" 61 | 62 | [tool.hatch.version] 63 | path = "jupyter_mcp_server/__version__.py" 64 | 65 | [tool.hatch.build] 66 | include = [ 67 | "jupyter_mcp_server/**/*.py", 68 | "jupyter-config/**/*.json" 69 | ] 70 | 71 | [tool.hatch.build.targets.wheel.shared-data] 72 | "jupyter-config/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d" 73 | "jupyter-config/jupyter_notebook_config.d" = "etc/jupyter/jupyter_notebook_config.d" 74 | 75 | [tool.pytest.ini_options] 76 | filterwarnings = [ 77 | "error", 78 | "ignore:There is no current event loop:DeprecationWarning", 79 | "module:make_current is deprecated:DeprecationWarning", 80 | "module:clear_current is deprecated:DeprecationWarning", 81 | "module:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", 82 | ] 83 | 84 | [tool.mypy] 85 | check_untyped_defs = true 86 | disallow_incomplete_defs = true 87 | no_implicit_optional = true 88 | pretty = true 89 | show_error_context = true 90 | show_error_codes = true 91 | strict_equality = true 92 | warn_unused_configs = true 93 | warn_unused_ignores = true 94 | warn_redundant_casts = true 95 | 96 | [tool.ruff] 97 | target-version = "py310" 98 | line-length = 100 99 | 100 | [tool.ruff.lint] 101 | select = [ 102 | "A", 103 | "B", 104 | "C", 105 | "E", 106 | "F", 107 | "FBT", 108 | "I", 109 | "N", 110 | "Q", 111 | "RUF", 112 | "S", 113 | "T", 114 | "UP", 115 | "W", 116 | "YTT", 117 | ] 118 | ignore = [ 119 | # FBT001 Boolean positional arg in function definition 120 | "FBT001", 121 | "FBT002", 122 | "FBT003", 123 | ] 124 | 125 | [tool.ruff.lint.per-file-ignores] 126 | # S101 Use of `assert` detected 127 | "jupyter_mcp_server/tests/*" = ["S101"] 128 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/streamable-http/standalone/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # As a Standalone Server 2 | 3 | ## 1. Start JupyterLab 4 | 5 | ### Environment setup 6 | 7 | Make sure you have the following packages installed in your environment. The collaboration package is needed as the modifications made on the notebook can be seen thanks to [Jupyter Real Time Collaboration](https://jupyterlab.readthedocs.io/en/stable/user/rtc.html). 8 | 9 | ```bash 10 | pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel 11 | pip uninstall -y pycrdt datalayer_pycrdt 12 | pip install datalayer_pycrdt==0.12.17 13 | ``` 14 | 15 | ### JupyterLab start 16 | 17 | Then, start JupyterLab with the following command. 18 | 19 | ```bash 20 | jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0 21 | ``` 22 | 23 | You can also run `make jupyterlab` if you cloned the repository. 24 | 25 | :::note 26 | 27 | The `--ip` is set to `0.0.0.0` to allow the MCP Server running in a Docker container to access your local JupyterLab. 28 | 29 | ::: 30 | 31 | :::info 32 | For JupyterHub: 33 | - Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment. 34 | - Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub. 35 | ::: 36 | 37 | ## 2. Setup Jupyter MCP Server 38 | 39 | Jupyter MCP Server also supports streamable HTTP transport, which allows you to connect to the Jupyter MCP Server using a URL. To start the server, you can either use `python` or `docker`. The server will listen on port `4040`, you can access it via http://localhost:4040. 40 | 41 | ### Run the Jupyter MCP Server 42 | 43 | #### Run with Python 44 | 45 | Clone the repository and use `pip install -e .` or just install the `jupyter-mcp-server package` from PyPI with `pip install jupyter-mcp-server`. 46 | 47 | Then, you can start Jupyter MCP Server with the following commands. 48 | 49 | ```bash 50 | # make start 51 | jupyter-mcp-server start \ 52 | --transport streamable-http \ 53 | --document-url http://localhost:8888 \ 54 | --document-id notebook.ipynb \ 55 | --document-token MY_TOKEN \ 56 | --runtime-url http://localhost:8888 \ 57 | --start-new-runtime true \ 58 | --runtime-token MY_TOKEN \ 59 | --port 4040 60 | ``` 61 | 62 | #### Run with Docker 63 | 64 | :::note 65 | 66 | You can set the `DOCUMENT_URL` (JupyterLab Server URL), `RUNTIME_TOKEN`, and `DOCUMENT_ID` environment variables to configure the server with the `-e` option in the `docker run` command. If not set, the defaults will be used. Refer to the [server configuration](/configure) for more details on the available environment variables and their defaults. 67 | 68 | ::: 69 | 70 | You can also run the Jupyter MCP Server using Docker. Use the following command to start the server on **MacOS**. 71 | 72 | ```bash 73 | docker run \ 74 | -e DOCUMENT_URL="http://localhost:8888" \ 75 | -p 4040:4040 \ 76 | datalayer/jupyter-mcp-server:latest \ 77 | --transport streamable-http 78 | ``` 79 | 80 | Use the following command to start the server on **Linux**. 81 | 82 | ```bash 83 | docker run \ 84 | --network=host \ 85 | -e DOCUMENT_URL="http://localhost:8888" \ 86 | -p 4040:4040 \ 87 | datalayer/jupyter-mcp-server:latest \ 88 | --transport streamable-http 89 | ``` 90 | 91 | <!-- 92 | 93 | ## Run with Smithery 94 | 95 | To install Jupyter MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@datalayer/jupyter-mcp-server): 96 | 97 | ```bash 98 | npx -y @smithery/cli install @datalayer/jupyter-mcp-server --client claude 99 | ``` 100 | 101 | --> 102 | 103 | ### Configure your MCP Client 104 | 105 | Use the following configuration for you MCP client to connect to a running Jupyter MCP Server. 106 | 107 | ```json 108 | { 109 | "mcpServers": { 110 | "jupyter": { 111 | "command": "npx", 112 | "args": ["mcp-remote", "http://127.0.0.1:4040/mcp"] 113 | } 114 | } 115 | } 116 | ``` 117 | ``` -------------------------------------------------------------------------------- /docs/docs/clients/vscode/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # VS Code 2 | 3 | You can find the complete VS Code MCP documentation [here](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_use-mcp-tools-in-agent-mode). 4 | 5 | ## Install VS Code 6 | 7 | Download VS Code from the [official site](https://code.visualstudio.com/Download) and install it. 8 | 9 | ## Install GitHub Copilot Extension 10 | 11 | To use MCP tools and Agent mode in VS Code, you need an active [GitHub Copilot](https://github.com/features/copilot) subscription. Then, install the [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extension from the VS Code Marketplace. 12 | 13 | ## Configure Jupyter MCP Server 14 | 15 | There are two ways to configure the Jupyter MCP Server in VS Code: user settings or workspace settings. Once configured, restart VS Code. 16 | 17 | :::note 18 | 19 | We explicitely use the name `DatalayerJupyter` as VS Code has already a `Jupyter` MCP Server configured by default for the VS Code built-in notebooks. 20 | 21 | ::: 22 | 23 | ### As User Settings in `settings.json` 24 | 25 | Open your `settings.json`: 26 | 27 | - Press `Ctrl+Shift+P` (or `⌘⇧P` on macOS) to open the **Command Palette** 28 | - Type and select: **Preferences: Open Settings (JSON)** 29 | [Or click this command link inside VS Code](command:workbench.action.openSettingsJson) 30 | 31 | Then add the following configuration: 32 | 33 | ```jsonc 34 | { 35 | "mcp": { 36 | "servers": { 37 | "DatalayerJupyter": { 38 | "command": "docker", 39 | "args": [ 40 | "run", 41 | "-i", 42 | "--rm", 43 | "-e", 44 | "DOCUMENT_URL", 45 | "-e", 46 | "DOCUMENT_TOKEN", 47 | "-e", 48 | "DOCUMENT_ID", 49 | "-e", 50 | "RUNTIME_URL", 51 | "-e", 52 | "RUNTIME_TOKEN", 53 | "datalayer/jupyter-mcp-server:latest" 54 | ], 55 | "env": { 56 | "DOCUMENT_URL": "http://host.docker.internal:8888", 57 | "DOCUMENT_TOKEN": "MY_TOKEN", 58 | "DOCUMENT_ID": "notebook.ipynb", 59 | "RUNTIME_URL": "http://host.docker.internal:8888", 60 | "RUNTIME_TOKEN": "MY_TOKEN" 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | Update with the actual configuration details from the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server). 68 | 69 | ### As Workspace Settings in `.vscode/mcp.json` 70 | 71 | Open or create a `.vscode/mcp.json` file in your workspace root directory. Then add the following example configuration: 72 | 73 | ```jsonc 74 | { 75 | "servers": { 76 | "DatalayerJupyter": { 77 | "command": "docker", 78 | "args": [ 79 | "run", 80 | "-i", 81 | "--rm", 82 | "-e", 83 | "DOCUMENT_URL", 84 | "-e", 85 | "DOCUMENT_TOKEN", 86 | "-e", 87 | "DOCUMENT_ID", 88 | "-e", 89 | "RUNTIME_URL", 90 | "-e", 91 | "RUNTIME_TOKEN", 92 | "datalayer/jupyter-mcp-server:latest" 93 | ], 94 | "env": { 95 | "DOCUMENT_URL": "http://host.docker.internal:8888", 96 | "DOCUMENT_TOKEN": "MY_TOKEN", 97 | "DOCUMENT_ID": "notebook.ipynb", 98 | "RUNTIME_URL": "http://host.docker.internal:8888", 99 | "RUNTIME_TOKEN": "MY_TOKEN" 100 | } 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | Update with the actual configuration details from the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server). 107 | 108 | This enables workspace-specific configuration and sharing. 109 | 110 | ## Use MCP Tools in Agent Mode 111 | 112 | 1. Launch Copilot Chat (`Ctrl+Alt+I` / `⌃⌘I`) 113 | 2. Switch to **Agent** mode from the dropdown 114 | 3. Click the **Tools** ⚙️ icon to manage Jupyter MCP Server tools 115 | 4. Use `#toolName` to invoke tools manually, or let Copilot invoke them automatically 116 | 5. Confirm tool actions when prompted (once or always) 117 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/restart_notebook_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Restart notebook tool implementation.""" 6 | 7 | import logging 8 | from typing import Any, Optional 9 | from jupyter_server_api import JupyterServerClient 10 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 11 | from jupyter_mcp_server.notebook_manager import NotebookManager 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class RestartNotebookTool(BaseTool): 17 | """Tool to restart the kernel for a specific notebook. 18 | 19 | Supports both MCP_SERVER and JUPYTER_SERVER modes. 20 | """ 21 | 22 | @property 23 | def name(self) -> str: 24 | return "restart_notebook" 25 | 26 | @property 27 | def description(self) -> str: 28 | return """Restart the kernel for a specific notebook. 29 | 30 | Args: 31 | notebook_name: Notebook identifier to restart 32 | 33 | Returns: 34 | str: Success message""" 35 | 36 | async def execute( 37 | self, 38 | mode: ServerMode, 39 | server_client: Optional[JupyterServerClient] = None, 40 | kernel_client: Optional[Any] = None, 41 | contents_manager: Optional[Any] = None, 42 | kernel_manager: Optional[Any] = None, 43 | kernel_spec_manager: Optional[Any] = None, 44 | notebook_manager: Optional[NotebookManager] = None, 45 | # Tool-specific parameters 46 | notebook_name: str = None, 47 | **kwargs 48 | ) -> str: 49 | """Execute the restart_notebook tool. 50 | 51 | Args: 52 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 53 | kernel_manager: Kernel manager for JUPYTER_SERVER mode 54 | notebook_manager: Notebook manager instance 55 | notebook_name: Notebook identifier to restart 56 | **kwargs: Additional parameters 57 | 58 | Returns: 59 | Success message 60 | """ 61 | if notebook_name not in notebook_manager: 62 | return f"Notebook '{notebook_name}' is not connected." 63 | 64 | if mode == ServerMode.JUPYTER_SERVER: 65 | # JUPYTER_SERVER mode: Use kernel_manager to restart the kernel 66 | if kernel_manager is None: 67 | return f"Failed to restart notebook '{notebook_name}': kernel_manager is required in JUPYTER_SERVER mode." 68 | 69 | # Get kernel ID from notebook_manager 70 | kernel_id = notebook_manager.get_kernel_id(notebook_name) 71 | if not kernel_id: 72 | return f"Failed to restart notebook '{notebook_name}': kernel ID not found." 73 | 74 | try: 75 | logger.info(f"Restarting kernel {kernel_id} for notebook '{notebook_name}' in JUPYTER_SERVER mode") 76 | await kernel_manager.restart_kernel(kernel_id) 77 | return f"Notebook '{notebook_name}' kernel restarted successfully. Memory state and imported packages have been cleared." 78 | except Exception as e: 79 | logger.error(f"Failed to restart kernel {kernel_id}: {e}") 80 | return f"Failed to restart notebook '{notebook_name}': {e}" 81 | 82 | elif mode == ServerMode.MCP_SERVER: 83 | # MCP_SERVER mode: Use notebook_manager's restart_notebook method 84 | success = notebook_manager.restart_notebook(notebook_name) 85 | 86 | if success: 87 | return f"Notebook '{notebook_name}' kernel restarted successfully. Memory state and imported packages have been cleared." 88 | else: 89 | return f"Failed to restart notebook '{notebook_name}'. The kernel may not support restart operation." 90 | else: 91 | return f"Invalid mode: {mode}" 92 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/_registry.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Tool registry and integration module.""" 6 | 7 | from typing import Dict, Any, Optional 8 | from jupyter_server_api import JupyterServerClient 9 | from jupyter_kernel_client import KernelClient 10 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 11 | from jupyter_mcp_server.notebook_manager import NotebookManager 12 | from jupyter_mcp_server.config import get_config 13 | 14 | 15 | class ToolRegistry: 16 | """Registry for managing and executing MCP tools.""" 17 | 18 | def __init__(self): 19 | self._tools: Dict[str, BaseTool] = {} 20 | self._notebook_manager: Optional[NotebookManager] = None 21 | 22 | def register(self, tool: BaseTool): 23 | """Register a tool instance.""" 24 | self._tools[tool.name] = tool 25 | 26 | def set_notebook_manager(self, notebook_manager: NotebookManager): 27 | """Set the notebook manager instance.""" 28 | self._notebook_manager = notebook_manager 29 | 30 | def get_tool(self, name: str) -> Optional[BaseTool]: 31 | """Get a tool by name.""" 32 | return self._tools.get(name) 33 | 34 | def list_tools(self): 35 | """List all registered tools.""" 36 | return list(self._tools.values()) 37 | 38 | async def execute_tool( 39 | self, 40 | tool_name: str, 41 | mode: ServerMode, 42 | **kwargs 43 | ) -> Any: 44 | """Execute a tool by name. 45 | 46 | Args: 47 | tool_name: Name of the tool to execute 48 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 49 | **kwargs: Tool-specific parameters 50 | 51 | Returns: 52 | Tool execution result 53 | """ 54 | tool = self.get_tool(tool_name) 55 | if not tool: 56 | raise ValueError(f"Tool '{tool_name}' not found") 57 | 58 | # Prepare common parameters based on mode 59 | config = get_config() 60 | 61 | if mode == ServerMode.MCP_SERVER: 62 | # Create HTTP clients for remote access 63 | server_client = JupyterServerClient( 64 | base_url=config.runtime_url, 65 | token=config.runtime_token 66 | ) 67 | kernel_client = KernelClient( 68 | server_url=config.runtime_url, 69 | token=config.runtime_token, 70 | kernel_id=config.runtime_id 71 | ) 72 | 73 | return await tool.execute( 74 | mode=mode, 75 | server_client=server_client, 76 | kernel_client=kernel_client, 77 | contents_manager=None, 78 | kernel_manager=None, 79 | kernel_spec_manager=None, 80 | notebook_manager=self._notebook_manager, 81 | server_url=config.runtime_url, 82 | token=config.runtime_token, 83 | **kwargs 84 | ) 85 | 86 | else: # JUPYTER_SERVER mode 87 | # Get managers from ServerContext 88 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 89 | context = get_server_context() 90 | 91 | contents_manager = context.get_contents_manager() 92 | kernel_manager = context.get_kernel_manager() 93 | kernel_spec_manager = context.get_kernel_spec_manager() 94 | 95 | return await tool.execute( 96 | mode=mode, 97 | server_client=None, 98 | kernel_client=None, 99 | contents_manager=contents_manager, 100 | kernel_manager=kernel_manager, 101 | kernel_spec_manager=kernel_spec_manager, 102 | notebook_manager=self._notebook_manager, 103 | server_url=config.runtime_url, 104 | token=config.runtime_token, 105 | **kwargs 106 | ) 107 | 108 | 109 | # Global registry instance 110 | _registry = ToolRegistry() 111 | 112 | 113 | def get_tool_registry() -> ToolRegistry: 114 | """Get the global tool registry instance.""" 115 | return _registry 116 | 117 | 118 | def register_tool(tool: BaseTool): 119 | """Register a tool with the global registry.""" 120 | _registry.register(tool) 121 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/unuse_notebook_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Unuse notebook tool implementation.""" 6 | 7 | import logging 8 | from typing import Any, Optional 9 | from jupyter_server_api import JupyterServerClient 10 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 11 | from jupyter_mcp_server.notebook_manager import NotebookManager 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class UnuseNotebookTool(BaseTool): 17 | """Tool to unuse from a notebook and release its resources. 18 | 19 | Supports both MCP_SERVER and JUPYTER_SERVER modes. 20 | """ 21 | 22 | @property 23 | def name(self) -> str: 24 | return "unuse_notebook" 25 | 26 | @property 27 | def description(self) -> str: 28 | return """Unuse a specific notebook and release its resources. 29 | 30 | Args: 31 | notebook_name: Notebook identifier to unuse 32 | 33 | Returns: 34 | str: Success message""" 35 | 36 | async def execute( 37 | self, 38 | mode: ServerMode, 39 | server_client: Optional[JupyterServerClient] = None, 40 | kernel_client: Optional[Any] = None, 41 | contents_manager: Optional[Any] = None, 42 | kernel_manager: Optional[Any] = None, 43 | kernel_spec_manager: Optional[Any] = None, 44 | notebook_manager: Optional[NotebookManager] = None, 45 | # Tool-specific parameters 46 | notebook_name: str = None, 47 | **kwargs 48 | ) -> str: 49 | """Execute the unuse_notebook tool. 50 | 51 | Args: 52 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 53 | kernel_manager: Kernel manager for JUPYTER_SERVER mode (optional kernel shutdown) 54 | notebook_manager: Notebook manager instance 55 | notebook_name: Notebook identifier to disconnect 56 | **kwargs: Additional parameters 57 | 58 | Returns: 59 | Success message 60 | """ 61 | if notebook_name not in notebook_manager: 62 | return f"Notebook '{notebook_name}' is not connected." 63 | 64 | # Get info about which notebook was current 65 | current_notebook = notebook_manager.get_current_notebook() 66 | was_current = current_notebook == notebook_name 67 | 68 | if mode == ServerMode.JUPYTER_SERVER: 69 | # JUPYTER_SERVER mode: Optionally shutdown kernel before removing 70 | # Note: In JUPYTER_SERVER mode, kernel lifecycle is managed by kernel_manager 71 | # We only remove the reference in notebook_manager, the actual kernel 72 | # continues to run unless explicitly shutdown 73 | 74 | kernel_id = notebook_manager.get_kernel_id(notebook_name) 75 | if kernel_id and kernel_manager: 76 | try: 77 | logger.info(f"Notebook '{notebook_name}' is being unused in JUPYTER_SERVER mode. Kernel {kernel_id} remains running.") 78 | # Optional: Uncomment to shutdown kernel when unused 79 | # await kernel_manager.shutdown_kernel(kernel_id) 80 | # logger.info(f"Kernel {kernel_id} shutdown successfully") 81 | except Exception as e: 82 | logger.warning(f"Note: Could not access kernel {kernel_id}: {e}") 83 | 84 | success = notebook_manager.remove_notebook(notebook_name) 85 | 86 | elif mode == ServerMode.MCP_SERVER: 87 | # MCP_SERVER mode: Use notebook_manager's remove_notebook method 88 | # which handles KernelClient cleanup automatically 89 | success = notebook_manager.remove_notebook(notebook_name) 90 | else: 91 | return f"Invalid mode: {mode}" 92 | 93 | if success: 94 | message = f"Notebook '{notebook_name}' unused successfully." 95 | 96 | if was_current: 97 | new_current = notebook_manager.get_current_notebook() 98 | if new_current: 99 | message += f" Current notebook switched to '{new_current}'." 100 | else: 101 | message += " No notebooks remaining." 102 | 103 | return message 104 | else: 105 | return f"Notebook '{notebook_name}' was not found." 106 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/config.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | from typing import Optional 6 | from pydantic import BaseModel, Field 7 | 8 | 9 | class JupyterMCPConfig(BaseModel): 10 | """Singleton configuration object for Jupyter MCP Server.""" 11 | 12 | # Transport configuration 13 | transport: str = Field(default="stdio", description="The transport to use for the MCP server") 14 | 15 | # Provider configuration 16 | provider: str = Field(default="jupyter", description="The provider to use for the document and runtime") 17 | 18 | # Runtime configuration 19 | runtime_url: str = Field(default="http://localhost:8888", description="The runtime URL to use, or 'local' for direct serverapp access") 20 | start_new_runtime: bool = Field(default=False, description="Start a new runtime or use an existing one") 21 | runtime_id: Optional[str] = Field(default=None, description="The kernel ID to use") 22 | runtime_token: Optional[str] = Field(default=None, description="The runtime token to use for authentication") 23 | 24 | # Document configuration 25 | document_url: str = Field(default="http://localhost:8888", description="The document URL to use, or 'local' for direct serverapp access") 26 | document_id: Optional[str] = Field(default=None, description="The document id to use. Optional - if omitted, can list and select notebooks interactively") 27 | document_token: Optional[str] = Field(default=None, description="The document token to use for authentication") 28 | 29 | # Server configuration 30 | port: int = Field(default=4040, description="The port to use for the Streamable HTTP transport") 31 | 32 | class Config: 33 | """Pydantic configuration.""" 34 | validate_assignment = True 35 | arbitrary_types_allowed = True 36 | 37 | def is_local_document(self) -> bool: 38 | """Check if document URL is set to local.""" 39 | return self.document_url == "local" 40 | 41 | def is_local_runtime(self) -> bool: 42 | """Check if runtime URL is set to local.""" 43 | return self.runtime_url == "local" 44 | 45 | 46 | # Singleton instance 47 | _config_instance: Optional[JupyterMCPConfig] = None 48 | 49 | 50 | def get_config() -> JupyterMCPConfig: 51 | """Get the singleton configuration instance.""" 52 | global _config_instance 53 | if _config_instance is None: 54 | _config_instance = JupyterMCPConfig() 55 | return _config_instance 56 | 57 | 58 | def set_config(**kwargs) -> JupyterMCPConfig: 59 | """Set configuration values and return the config instance. 60 | 61 | Automatically handles string representations of None by removing them from kwargs, 62 | allowing defaults to be used instead. This handles cases where environment variables 63 | or MCP clients pass "None" as a string. 64 | """ 65 | def should_skip(value): 66 | """Check if value is a string representation of None that should be skipped.""" 67 | return isinstance(value, str) and value.lower() in ("none", "null", "") 68 | 69 | # Filter out string "None" values and let defaults be used instead 70 | # For optional fields (tokens, runtime_id, document_id), convert to actual None 71 | normalized_kwargs = {} 72 | for key, value in kwargs.items(): 73 | if should_skip(value): 74 | # For optional fields, set to None; for required fields, skip (use default) 75 | if key in ("runtime_token", "document_token", "runtime_id", "document_id"): 76 | normalized_kwargs[key] = None 77 | # For required string fields like runtime_url, document_url, skip the key 78 | # to let the default value be used 79 | # Do nothing - skip this key 80 | else: 81 | normalized_kwargs[key] = value 82 | 83 | global _config_instance 84 | if _config_instance is None: 85 | _config_instance = JupyterMCPConfig(**normalized_kwargs) 86 | else: 87 | for key, value in normalized_kwargs.items(): 88 | if hasattr(_config_instance, key): 89 | setattr(_config_instance, key, value) 90 | return _config_instance 91 | 92 | 93 | def reset_config() -> JupyterMCPConfig: 94 | """Reset configuration to defaults.""" 95 | global _config_instance 96 | _config_instance = JupyterMCPConfig() 97 | return _config_instance 98 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/enroll.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Auto-enrollment functionality for Jupyter MCP Server.""" 6 | 7 | import logging 8 | from typing import Any 9 | 10 | from jupyter_mcp_server.notebook_manager import NotebookManager 11 | from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | async def auto_enroll_document( 17 | config: Any, 18 | notebook_manager: NotebookManager, 19 | use_notebook_tool: UseNotebookTool, 20 | server_context: Any, 21 | ) -> None: 22 | """Automatically enroll the configured document_id as a managed notebook. 23 | 24 | Handles kernel creation/connection based on configuration: 25 | - If runtime_id is provided: Connect to that specific kernel 26 | - If start_new_runtime is True: Create a new kernel 27 | - If both are False/None: Enroll notebook WITHOUT kernel (notebook-only mode) 28 | 29 | Args: 30 | config: JupyterMCPConfig instance with configuration parameters 31 | notebook_manager: NotebookManager instance for managing notebooks 32 | use_notebook_tool: UseNotebookTool instance for enrolling notebooks 33 | server_context: ServerContext instance with server state 34 | """ 35 | # Check if document_id is configured and not already managed 36 | if not config.document_id: 37 | logger.debug("No document_id configured, skipping auto-enrollment") 38 | return 39 | 40 | if "default" in notebook_manager: 41 | logger.debug("Default notebook already enrolled, skipping auto-enrollment") 42 | return 43 | 44 | # Check if we should skip kernel creation entirely 45 | if not config.runtime_id and not config.start_new_runtime: 46 | # Enroll notebook without kernel - just register the notebook path 47 | try: 48 | logger.info(f"Auto-enrolling document '{config.document_id}' without kernel (notebook-only mode)") 49 | # Add notebook to manager without kernel 50 | notebook_manager.add_notebook( 51 | "default", 52 | None, # No kernel 53 | server_url=config.document_url, 54 | token=config.document_token, 55 | path=config.document_id 56 | ) 57 | notebook_manager.set_current_notebook("default") 58 | logger.info(f"Auto-enrollment result: Successfully enrolled notebook 'default' at path '{config.document_id}' without kernel.") 59 | return 60 | except Exception as e: 61 | logger.warning(f"Failed to auto-enroll document without kernel: {e}") 62 | return 63 | 64 | # Otherwise, enroll with kernel 65 | try: 66 | # Determine kernel_id based on configuration 67 | kernel_id_to_use = None 68 | if config.runtime_id: 69 | # User explicitly provided a kernel ID to connect to 70 | kernel_id_to_use = config.runtime_id 71 | logger.info(f"Auto-enrolling document '{config.document_id}' with existing kernel '{kernel_id_to_use}'") 72 | elif config.start_new_runtime: 73 | # User wants a new kernel created 74 | kernel_id_to_use = None # Will trigger new kernel creation in use_notebook_tool 75 | logger.info(f"Auto-enrolling document '{config.document_id}' with new kernel") 76 | 77 | # Use the use_notebook_tool to properly enroll the notebook with kernel 78 | result = await use_notebook_tool.execute( 79 | mode=server_context.mode, 80 | server_client=server_context.server_client, 81 | notebook_name="default", 82 | notebook_path=config.document_id, 83 | use_mode="connect", 84 | kernel_id=kernel_id_to_use, 85 | contents_manager=server_context.contents_manager, 86 | kernel_manager=server_context.kernel_manager, 87 | session_manager=server_context.session_manager, 88 | notebook_manager=notebook_manager, 89 | runtime_url=config.runtime_url if config.runtime_url != "local" else None, 90 | runtime_token=config.runtime_token, 91 | ) 92 | logger.info(f"Auto-enrollment result: {result}") 93 | except Exception as e: 94 | logger.warning(f"Failed to auto-enroll document: {e}. You can manually use it with use_notebook tool.") 95 | ``` -------------------------------------------------------------------------------- /examples/integration_example.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """ 6 | Example integration of the new tool architecture into server.py. 7 | 8 | This demonstrates how to: 9 | 1. Register tool instances with the registry 10 | 2. Wrap them with @mcp.tool() decorators 11 | 3. Determine the server mode and call tool.execute() 12 | """ 13 | 14 | from typing import Optional 15 | from jupyter_mcp_server.tools._base import ServerMode 16 | from jupyter_mcp_server.tools._registry import get_tool_registry, register_tool 17 | from jupyter_mcp_server.tools.list_notebooks_tool import ListNotebooksTool 18 | from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool 19 | 20 | 21 | # Initialize and register tools 22 | def initialize_tools(): 23 | """Register all tool instances.""" 24 | register_tool(ListNotebooksTool()) 25 | register_tool(UseNotebookTool()) 26 | # ... register other tools as they are created 27 | 28 | 29 | # Example of how to wrap a tool with @mcp.tool() decorator 30 | def register_mcp_tools(mcp, notebook_manager): 31 | """Register tools with FastMCP server. 32 | 33 | Args: 34 | mcp: FastMCP instance 35 | notebook_manager: NotebookManager instance 36 | """ 37 | registry = get_tool_registry() 38 | registry.set_notebook_manager(notebook_manager) 39 | 40 | @mcp.tool() 41 | async def list_notebook() -> str: 42 | """List all notebooks in the Jupyter server (including subdirectories) and show which ones are managed. 43 | 44 | To interact with a notebook, it has to be "managed". If a notebook is not managed, you can connect to it using the `use_notebook` tool. 45 | 46 | Returns: 47 | str: TSV formatted table with notebook information including management status 48 | """ 49 | # Determine server mode 50 | mode = _get_server_mode() 51 | 52 | # Execute the tool 53 | return await registry.execute_tool( 54 | "list_notebooks", 55 | mode=mode 56 | ) 57 | 58 | @mcp.tool() 59 | async def use_notebook( 60 | notebook_name: str, 61 | notebook_path: str, 62 | mode: str = "connect", # Renamed parameter to avoid conflict 63 | kernel_id: Optional[str] = None, 64 | ) -> str: 65 | """Connect to a notebook file or create a new one. 66 | 67 | Args: 68 | notebook_name: Unique identifier for the notebook 69 | notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb") 70 | mode: "connect" to connect to existing, "create" to create new 71 | kernel_id: Specific kernel ID to use (optional, will create new if not provided) 72 | 73 | Returns: 74 | str: Success message with notebook information 75 | """ 76 | # Determine server mode 77 | server_mode = _get_server_mode() 78 | 79 | # Execute the tool 80 | return await registry.execute_tool( 81 | "use_notebook", 82 | mode=server_mode, 83 | notebook_name=notebook_name, 84 | notebook_path=notebook_path, 85 | operation_mode=mode, # Map to tool's parameter name 86 | kernel_id=kernel_id 87 | ) 88 | 89 | # ... register other tools similarly 90 | 91 | 92 | def _get_server_mode() -> ServerMode: 93 | """Determine which server mode we're running in. 94 | 95 | Returns: 96 | ServerMode.JUPYTER_SERVER if running as Jupyter extension with local access 97 | ServerMode.MCP_SERVER if running standalone with HTTP clients 98 | """ 99 | try: 100 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 101 | context = get_server_context() 102 | 103 | # Check if we're in Jupyter server mode with local access 104 | if (context.context_type == "JUPYTER_SERVER" and 105 | context.is_local_document() and 106 | context.get_contents_manager() is not None): 107 | return ServerMode.JUPYTER_SERVER 108 | except ImportError: 109 | # Context module not available, must be MCP_SERVER mode 110 | pass 111 | except Exception: 112 | # Any error checking context, default to MCP_SERVER 113 | pass 114 | 115 | return ServerMode.MCP_SERVER 116 | 117 | 118 | # Example usage in server.py: 119 | # 120 | # # After creating mcp and notebook_manager instances: 121 | # initialize_tools() 122 | # register_mcp_tools(mcp, notebook_manager) 123 | ``` -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | /** @type {import('@docusaurus/types').DocusaurusConfig} */ 8 | module.exports = { 9 | title: '🪐 ✨ Jupyter MCP Server documentation', 10 | tagline: 'Tansform your Notebooks into an interactive, AI-powered workspace that adapts to your needs!', 11 | url: 'https://datalayer.ai', 12 | baseUrl: '/', 13 | onBrokenLinks: 'throw', 14 | onBrokenMarkdownLinks: 'warn', 15 | favicon: 'img/favicon.ico', 16 | organizationName: 'datalayer', // Usually your GitHub org/user name. 17 | projectName: 'jupyter-mcp-server', // Usually your repo name. 18 | markdown: { 19 | mermaid: true, 20 | }, 21 | plugins: [ 22 | '@docusaurus/theme-live-codeblock', 23 | 'docusaurus-lunr-search', 24 | ], 25 | themes: [ 26 | '@docusaurus/theme-mermaid', 27 | ], 28 | themeConfig: { 29 | colorMode: { 30 | defaultMode: 'light', 31 | disableSwitch: true, 32 | }, 33 | navbar: { 34 | title: 'Jupyter MCP Server Docs', 35 | logo: { 36 | alt: 'Datalayer Logo', 37 | src: 'img/datalayer/logo.svg', 38 | }, 39 | items: [ 40 | { 41 | href: 'https://discord.gg/YQFwvmSSuR', 42 | position: 'right', 43 | className: 'header-discord-link', 44 | 'aria-label': 'Discord', 45 | }, 46 | { 47 | href: 'https://github.com/datalayer/jupyter-mcp-server', 48 | position: 'right', 49 | className: 'header-github-link', 50 | 'aria-label': 'GitHub', 51 | }, 52 | { 53 | href: 'https://bsky.app/profile/datalayer.ai', 54 | position: 'right', 55 | className: 'header-bluesky-link', 56 | 'aria-label': 'Bluesky', 57 | }, 58 | { 59 | href: 'https://x.com/DatalayerIO', 60 | position: 'right', 61 | className: 'header-x-link', 62 | 'aria-label': 'X', 63 | }, 64 | { 65 | href: 'https://www.linkedin.com/company/datalayer', 66 | position: 'right', 67 | className: 'header-linkedin-link', 68 | 'aria-label': 'LinkedIn', 69 | }, 70 | { 71 | href: 'https://tiktok.com/@datalayerio', 72 | position: 'right', 73 | className: 'header-tiktok-link', 74 | 'aria-label': 'TikTok', 75 | }, 76 | { 77 | href: 'https://www.youtube.com/@datalayer', 78 | position: 'right', 79 | className: 'header-youtube-link', 80 | 'aria-label': 'YouTube', 81 | }, 82 | { 83 | href: 'https://datalayer.io', 84 | position: 'right', 85 | className: 'header-datalayer-io-link', 86 | 'aria-label': 'Datalayer', 87 | }, 88 | ], 89 | }, 90 | footer: { 91 | style: 'dark', 92 | links: [ 93 | { 94 | title: 'Docs', 95 | items: [ 96 | { 97 | label: 'Jupyter MCP Server', 98 | to: '/', 99 | }, 100 | ], 101 | }, 102 | { 103 | title: 'Community', 104 | items: [ 105 | { 106 | label: 'GitHub', 107 | href: 'https://github.com/datalayer', 108 | }, 109 | { 110 | label: 'Bluesky', 111 | href: 'https://assets.datalayer.tech/logos-social-grey/youtube.svg', 112 | }, 113 | { 114 | label: 'LinkedIn', 115 | href: 'https://www.linkedin.com/company/datalayer', 116 | }, 117 | ], 118 | }, 119 | { 120 | title: 'More', 121 | items: [ 122 | { 123 | label: 'Datalayer', 124 | href: 'https://datalayer.ai', 125 | }, 126 | { 127 | label: 'Datalayer Docs', 128 | href: 'https://docs.datalayer.ai', 129 | }, 130 | { 131 | label: 'Datalayer Blog', 132 | href: 'https://datalayer.blog', 133 | }, 134 | { 135 | label: 'Datalayer Guide', 136 | href: 'https://datalayer.guide', 137 | }, 138 | ], 139 | }, 140 | ], 141 | copyright: `Copyright © ${new Date().getFullYear()} Datalayer, Inc.`, 142 | }, 143 | }, 144 | presets: [ 145 | [ 146 | '@docusaurus/preset-classic', 147 | { 148 | docs: { 149 | routeBasePath: '/', 150 | docItemComponent: '@theme/CustomDocItem', 151 | sidebarPath: require.resolve('./sidebars.js'), 152 | editUrl: 'https://github.com/datalayer/jupyter-mcp-server/edit/main/', 153 | }, 154 | theme: { 155 | customCss: require.resolve('./src/css/custom.css'), 156 | }, 157 | gtag: { 158 | trackingID: 'G-EYRGHH1GN6', 159 | anonymizeIP: false, 160 | }, 161 | }, 162 | ], 163 | ], 164 | }; 165 | ``` -------------------------------------------------------------------------------- /docs/docs/deployment/jupyter/stdio/index.mdx: -------------------------------------------------------------------------------- ```markdown 1 | # STDIO Transport 2 | 3 | ## 1. Start JupyterLab 4 | 5 | ### Environment setup 6 | 7 | Make sure you have the following packages installed in your environment. The collaboration package is needed as the modifications made on the notebook can be seen thanks to [Jupyter Real Time Collaboration](https://jupyterlab.readthedocs.io/en/stable/user/rtc.html). 8 | 9 | ```bash 10 | pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel 11 | pip uninstall -y pycrdt datalayer_pycrdt 12 | pip install datalayer_pycrdt==0.12.17 13 | ``` 14 | 15 | ### JupyterLab start 16 | 17 | Then, start JupyterLab with the following command. 18 | 19 | ```bash 20 | jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0 21 | ``` 22 | 23 | You can also run `make jupyterlab` if you cloned the repository. 24 | 25 | :::note 26 | 27 | The `--ip` is set to `0.0.0.0` to allow the MCP Server running in a Docker container to access your local JupyterLab. 28 | 29 | ::: 30 | 31 | :::info 32 | For JupyterHub: 33 | - Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment. 34 | - Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub. 35 | ::: 36 | 37 | ## 2. Setup Jupyter MCP Server 38 | 39 | You can choose between two deployment methods: **uvx** (lightweight and faster, recommended for first try) or **Docker** (recommended for production). 40 | 41 | :::important 42 | 43 | - Ensure the `port` of the `DOCUMENT_URL` and `RUNTIME_URL` match those used in the `jupyter lab` command. 44 | 45 | - In a basic setup, `DOCUMENT_URL` and `RUNTIME_URL` are the same. `DOCUMENT_TOKEN`, and `RUNTIME_TOKEN` are also the same and are actually the Jupyter Token. 46 | 47 | - The `DOCUMENT_ID` parameter specifies the path to the notebook you want to connect to. It should be relative to the directory where JupyterLab was started. 48 | - **Optional:** If you omit `DOCUMENT_ID`, the MCP client can automatically list all available notebooks on the Jupyter server, allowing you to select one interactively via your prompts. 49 | - **Flexible:** Even if you set `DOCUMENT_ID`, the MCP client can still browse, list, switch to, or even create new notebooks at any time. 50 | 51 | - More environment variables can be set, refer to the [server configuration](/configure) for more details. If not specified, the server will use the default values. 52 | 53 | ::: 54 | 55 | ### Using UVX (Quick Start) 56 | 57 | Make sure you have `uv` installed. You can install it via pip: 58 | 59 | ```bash 60 | pip install uv 61 | uv --version 62 | # should be 0.6.14 or higher 63 | ``` 64 | See more details on [uv installation](https://docs.astral.sh/uv/getting-started/installation/). 65 | 66 | Use the following configuration file to set up the Jupyter MCP Server for your preferred MCP client. 67 | 68 | ```json 69 | { 70 | "mcpServers": { 71 | "jupyter": { 72 | "command": "uvx", 73 | "args": ["jupyter-mcp-server@latest"], 74 | "env": { 75 | "DOCUMENT_URL": "http://localhost:8888", 76 | "DOCUMENT_TOKEN": "MY_TOKEN", 77 | "DOCUMENT_ID": "notebook.ipynb", 78 | "RUNTIME_URL": "http://localhost:8888", 79 | "RUNTIME_TOKEN": "MY_TOKEN", 80 | "ALLOW_IMG_OUTPUT": "true" 81 | } 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ### Using Docker (Production) 88 | 89 | Use the following configuration file to set up the Jupyter MCP Server for your preferred MCP client. Note that the configuration is dependent on the operating system you are using. 90 | 91 | 92 | #### For MacOS and Windows 93 | 94 | ```json 95 | { 96 | "mcpServers": { 97 | "jupyter": { 98 | "command": "docker", 99 | "args": [ 100 | "run", "-i", "--rm", 101 | "-e", "DOCUMENT_URL", 102 | "-e", "DOCUMENT_TOKEN", 103 | "-e", "DOCUMENT_ID", 104 | "-e", "RUNTIME_URL", 105 | "-e", "RUNTIME_TOKEN", 106 | "datalayer/jupyter-mcp-server:latest" 107 | ], 108 | "env": { 109 | "DOCUMENT_URL": "http://host.docker.internal:8888", 110 | "DOCUMENT_TOKEN": "MY_TOKEN", 111 | "DOCUMENT_ID": "notebook.ipynb", 112 | "RUNTIME_URL": "http://host.docker.internal:8888", 113 | "RUNTIME_TOKEN": "MY_TOKEN" 114 | } 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | #### For Linux 121 | 122 | ```json 123 | { 124 | "mcpServers": { 125 | "jupyter": { 126 | "command": "docker", 127 | "args": [ 128 | "run", "-i", "--rm", 129 | "-e", "DOCUMENT_URL", 130 | "-e", "DOCUMENT_TOKEN", 131 | "-e", "DOCUMENT_ID", 132 | "-e", "RUNTIME_URL", 133 | "-e", "RUNTIME_TOKEN", 134 | "--network=host", 135 | "datalayer/jupyter-mcp-server:latest" 136 | ], 137 | "env": { 138 | "DOCUMENT_URL": "http://localhost:8888", 139 | "DOCUMENT_TOKEN": "MY_TOKEN", 140 | "DOCUMENT_ID": "notebook.ipynb", 141 | "RUNTIME_URL": "http://localhost:8888", 142 | "RUNTIME_TOKEN": "MY_TOKEN" 143 | } 144 | } 145 | } 146 | } 147 | ``` 148 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/read_cells_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Read all cells tool implementation.""" 6 | 7 | from typing import Any, Optional, List, Dict, Union 8 | from jupyter_server_api import JupyterServerClient 9 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 10 | from jupyter_mcp_server.notebook_manager import NotebookManager 11 | from jupyter_mcp_server.models import CellInfo 12 | from jupyter_mcp_server.config import get_config 13 | from jupyter_mcp_server.utils import get_current_notebook_context 14 | from mcp.types import ImageContent 15 | 16 | 17 | class ReadCellsTool(BaseTool): 18 | """Tool to read cells from a Jupyter notebook.""" 19 | 20 | @property 21 | def name(self) -> str: 22 | return "read_cells" 23 | 24 | @property 25 | def description(self) -> str: 26 | return """Read cells from the Jupyter notebook. 27 | 28 | Returns: 29 | list[dict]: List of cell information including index, type, source, 30 | and outputs (for code cells)""" 31 | 32 | async def _read_cells_local(self, contents_manager: Any, path: str) -> List[Dict[str, Any]]: 33 | """Read cells using local contents_manager (JUPYTER_SERVER mode).""" 34 | # Read the notebook file directly 35 | model = await contents_manager.get(path, content=True, type='notebook') 36 | 37 | if 'content' not in model: 38 | raise ValueError(f"Could not read notebook content from {path}") 39 | 40 | notebook_content = model['content'] 41 | cells = notebook_content.get('cells', []) 42 | 43 | # Convert cells to the expected format using CellInfo for consistency 44 | result = [] 45 | for idx, cell in enumerate(cells): 46 | # Use CellInfo.from_cell to ensure consistent structure and output processing 47 | cell_info = CellInfo.from_cell(cell_index=idx, cell=cell) 48 | result.append(cell_info.model_dump(exclude_none=True)) 49 | 50 | return result 51 | 52 | async def execute( 53 | self, 54 | mode: ServerMode, 55 | server_client: Optional[JupyterServerClient] = None, 56 | kernel_client: Optional[Any] = None, 57 | contents_manager: Optional[Any] = None, 58 | kernel_manager: Optional[Any] = None, 59 | kernel_spec_manager: Optional[Any] = None, 60 | notebook_manager: Optional[NotebookManager] = None, 61 | **kwargs 62 | ) -> List[Dict[str, Union[str, int, List[Union[str, ImageContent]]]]]: 63 | """Execute the read_cells tool. 64 | 65 | Args: 66 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 67 | contents_manager: Direct API access for JUPYTER_SERVER mode 68 | notebook_manager: Notebook manager instance for MCP_SERVER mode 69 | **kwargs: Additional parameters 70 | 71 | Returns: 72 | List of cell information dictionaries 73 | """ 74 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None: 75 | # Local mode: read notebook directly from file system 76 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 77 | from pathlib import Path 78 | 79 | context = get_server_context() 80 | serverapp = context.serverapp 81 | 82 | notebook_path, _ = get_current_notebook_context(notebook_manager) 83 | 84 | # contents_manager expects path relative to serverapp.root_dir 85 | # If we have an absolute path, convert it to relative 86 | if serverapp and Path(notebook_path).is_absolute(): 87 | root_dir = Path(serverapp.root_dir) 88 | abs_path = Path(notebook_path) 89 | try: 90 | notebook_path = str(abs_path.relative_to(root_dir)) 91 | except ValueError: 92 | # Path is not under root_dir, use as-is 93 | pass 94 | 95 | return await self._read_cells_local(contents_manager, notebook_path) 96 | elif mode == ServerMode.MCP_SERVER and notebook_manager is not None: 97 | # Remote mode: use WebSocket connection to Y.js document 98 | async with notebook_manager.get_current_connection() as notebook: 99 | cells = [] 100 | total_cells = len(notebook) 101 | 102 | for i in range(total_cells): 103 | cells.append(CellInfo.from_cell(i, notebook[i]).model_dump(exclude_none=True)) 104 | 105 | return cells 106 | else: 107 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}") 108 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/read_cell_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Read cell tool implementation.""" 6 | 7 | from typing import Any, Optional, Dict, Union, List 8 | from jupyter_server_api import JupyterServerClient 9 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 10 | from jupyter_mcp_server.notebook_manager import NotebookManager 11 | from jupyter_mcp_server.models import CellInfo 12 | from jupyter_mcp_server.config import get_config 13 | from mcp.types import ImageContent 14 | 15 | 16 | class ReadCellTool(BaseTool): 17 | """Tool to read a specific cell from a notebook.""" 18 | 19 | @property 20 | def name(self) -> str: 21 | return "read_cell" 22 | 23 | @property 24 | def description(self) -> str: 25 | return """Read a specific cell from the Jupyter notebook. 26 | 27 | Args: 28 | cell_index: Index of the cell to read (0-based) 29 | 30 | Returns: 31 | dict: Cell information including index, type, source, and outputs (for code cells)""" 32 | 33 | async def _read_cell_local(self, contents_manager: Any, path: str, cell_index: int) -> Dict[str, Any]: 34 | """Read a specific cell using local contents_manager (JUPYTER_SERVER mode).""" 35 | # Read the notebook file directly 36 | model = await contents_manager.get(path, content=True, type='notebook') 37 | 38 | if 'content' not in model: 39 | raise ValueError(f"Could not read notebook content from {path}") 40 | 41 | notebook_content = model['content'] 42 | cells = notebook_content.get('cells', []) 43 | 44 | if cell_index < 0 or cell_index >= len(cells): 45 | raise ValueError( 46 | f"Cell index {cell_index} is out of range. Notebook has {len(cells)} cells." 47 | ) 48 | 49 | cell = cells[cell_index] 50 | 51 | # Use CellInfo.from_cell to normalize the structure (ensures "type" field not "cell_type") 52 | cell_info = CellInfo.from_cell(cell_index=cell_index, cell=cell) 53 | 54 | return cell_info.model_dump(exclude_none=True) 55 | 56 | async def execute( 57 | self, 58 | mode: ServerMode, 59 | server_client: Optional[JupyterServerClient] = None, 60 | kernel_client: Optional[Any] = None, 61 | contents_manager: Optional[Any] = None, 62 | kernel_manager: Optional[Any] = None, 63 | kernel_spec_manager: Optional[Any] = None, 64 | notebook_manager: Optional[NotebookManager] = None, 65 | # Tool-specific parameters 66 | cell_index: int = None, 67 | **kwargs 68 | ) -> Dict[str, Union[str, int, List[Union[str, ImageContent]]]]: 69 | """Execute the read_cell tool. 70 | 71 | Args: 72 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 73 | contents_manager: Direct API access for JUPYTER_SERVER mode 74 | notebook_manager: Notebook manager instance 75 | cell_index: Index of the cell to read (0-based) 76 | **kwargs: Additional parameters 77 | 78 | Returns: 79 | Cell information dictionary 80 | """ 81 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None: 82 | # Use local contents_manager to read the notebook 83 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 84 | from pathlib import Path 85 | 86 | context = get_server_context() 87 | serverapp = context.serverapp 88 | 89 | notebook_path = None 90 | if notebook_manager: 91 | notebook_path = notebook_manager.get_current_notebook_path() 92 | if not notebook_path: 93 | config = get_config() 94 | notebook_path = config.document_id 95 | 96 | # contents_manager expects path relative to serverapp.root_dir 97 | # If we have an absolute path, convert it to relative 98 | if serverapp and Path(notebook_path).is_absolute(): 99 | root_dir = Path(serverapp.root_dir) 100 | abs_path = Path(notebook_path) 101 | try: 102 | notebook_path = str(abs_path.relative_to(root_dir)) 103 | except ValueError: 104 | # Path is not under root_dir, use as-is 105 | pass 106 | 107 | return await self._read_cell_local(contents_manager, notebook_path, cell_index) 108 | elif mode == ServerMode.MCP_SERVER and notebook_manager is not None: 109 | # Remote mode: use WebSocket connection to Y.js document 110 | async with notebook_manager.get_current_connection() as notebook: 111 | if cell_index < 0 or cell_index >= len(notebook): 112 | raise ValueError(f"Cell index {cell_index} out of range") 113 | 114 | cell = notebook[cell_index] 115 | return CellInfo.from_cell(cell_index=cell_index, cell=cell).model_dump(exclude_none=True) 116 | else: 117 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}") 118 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/list_files_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """List all files and directories tool.""" 6 | 7 | from typing import Any, Optional, List, Dict 8 | from jupyter_server_api import JupyterServerClient 9 | 10 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 11 | from jupyter_mcp_server.config import get_config 12 | from jupyter_mcp_server.utils import format_TSV 13 | 14 | 15 | class ListFilesTool(BaseTool): 16 | """List files and directories in the Jupyter server's file system. 17 | 18 | This tool recursively lists files and directories from the Jupyter server's content API, 19 | showing the complete file structure including notebooks, data files, scripts, and directories. 20 | """ 21 | 22 | @property 23 | def name(self) -> str: 24 | return "list_files" 25 | 26 | @property 27 | def description(self) -> str: 28 | return "List all files and directories in the Jupyter server's file system" 29 | 30 | async def _list_files_local( 31 | self, 32 | contents_manager: Any, 33 | path: str = "", 34 | max_depth: int = 3, 35 | current_depth: int = 0 36 | ) -> List[Dict[str, Any]]: 37 | """List files using local contents_manager API (JUPYTER_SERVER mode).""" 38 | all_files = [] 39 | 40 | if current_depth >= max_depth: 41 | return all_files 42 | 43 | try: 44 | # Get directory contents 45 | model = await contents_manager.get(path, content=True, type='directory') 46 | 47 | if 'content' not in model: 48 | return all_files 49 | 50 | for item in model['content']: 51 | item_path = item['path'] 52 | item_type = item['type'] 53 | 54 | file_info = { 55 | 'path': item_path, 56 | 'type': item_type, 57 | 'size': item.get('size', 0) if item_type == 'file' else 0, 58 | 'last_modified': item.get('last_modified', '') 59 | } 60 | all_files.append(file_info) 61 | 62 | # Recursively list subdirectories 63 | if item_type == 'directory': 64 | subfiles = await self._list_files_local( 65 | contents_manager, 66 | item_path, 67 | max_depth, 68 | current_depth + 1 69 | ) 70 | all_files.extend(subfiles) 71 | 72 | except Exception: 73 | # Directory not accessible or doesn't exist 74 | pass 75 | 76 | return all_files 77 | 78 | async def execute( 79 | self, 80 | mode: ServerMode, 81 | server_client: Optional[JupyterServerClient] = None, 82 | kernel_client: Optional[Any] = None, 83 | contents_manager: Optional[Any] = None, 84 | kernel_manager: Optional[Any] = None, 85 | kernel_spec_manager: Optional[Any] = None, 86 | notebook_manager: Optional[Any] = None, 87 | # Tool-specific parameters 88 | path: str = "", 89 | max_depth: int = 3, 90 | list_files_recursively_fn=None, 91 | **kwargs 92 | ) -> str: 93 | """List all files and directories. 94 | 95 | Args: 96 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 97 | contents_manager: Direct API access for JUPYTER_SERVER mode 98 | path: The starting path to list from (empty string means root directory) 99 | max_depth: Maximum depth to recurse into subdirectories (default: 3) 100 | list_files_recursively_fn: Function to recursively list files (MCP_SERVER mode) 101 | **kwargs: Additional parameters 102 | 103 | Returns: 104 | Tab-separated table with columns: Path, Type, Size, Last_Modified 105 | """ 106 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None: 107 | # Local mode: use contents_manager directly 108 | all_files = await self._list_files_local(contents_manager, path, max_depth) 109 | elif mode == ServerMode.MCP_SERVER: 110 | # Remote mode: use HTTP client 111 | if list_files_recursively_fn is None: 112 | raise ValueError("list_files_recursively_fn is required for MCP_SERVER mode") 113 | 114 | config = get_config() 115 | server_client = JupyterServerClient(base_url=config.runtime_url, token=config.runtime_token) 116 | all_files = list_files_recursively_fn(server_client, path, 0, None, max_depth) 117 | else: 118 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}") 119 | 120 | if not all_files: 121 | return f"No files found in path '{path or 'root'}'" 122 | 123 | # Sort files by path for better readability 124 | all_files.sort(key=lambda x: x['path']) 125 | 126 | # Create TSV formatted output 127 | headers = ["Path", "Type", "Size", "Last_Modified"] 128 | rows = [] 129 | for file_info in all_files: 130 | rows.append([file_info['path'], file_info['type'], file_info['size'], file_info['last_modified']]) 131 | 132 | return format_TSV(headers, rows) 133 | ``` -------------------------------------------------------------------------------- /docs/static/img/datalayer/logo.svg: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- 3 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 4 | ~ 5 | ~ BSD 3-Clause License 6 | --> 7 | 8 | <!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 9 | 10 | <svg 11 | xmlns:dc="http://purl.org/dc/elements/1.1/" 12 | xmlns:cc="http://creativecommons.org/ns#" 13 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 14 | xmlns:svg="http://www.w3.org/2000/svg" 15 | xmlns="http://www.w3.org/2000/svg" 16 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 18 | version="1.1" 19 | x="0px" 20 | y="0px" 21 | width="100" 22 | height="100" 23 | viewBox="0 0 99.999997 99.999999" 24 | enable-background="new 0 0 130.395 175.748" 25 | xml:space="preserve" 26 | id="svg1104" 27 | sodipodi:docname="logo_square.svg" 28 | inkscape:version="0.92.2 5c3e80d, 2017-08-06" 29 | inkscape:export-filename="/Users/echar4/private/marketing/datalayer/logo/corporate/png/logo_square.png" 30 | inkscape:export-xdpi="300" 31 | inkscape:export-ydpi="300"><metadata 32 | id="metadata1110"><rdf:RDF><cc:Work 33 | rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type 34 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs 35 | id="defs1108" /><sodipodi:namedview 36 | pagecolor="#ffffff" 37 | bordercolor="#666666" 38 | borderopacity="1" 39 | objecttolerance="10" 40 | gridtolerance="10" 41 | guidetolerance="10" 42 | inkscape:pageopacity="0" 43 | inkscape:pageshadow="2" 44 | inkscape:window-width="1406" 45 | inkscape:window-height="746" 46 | id="namedview1106" 47 | showgrid="false" 48 | inkscape:zoom="0.94952545" 49 | inkscape:cx="24.718555" 50 | inkscape:cy="60.203158" 51 | inkscape:window-x="0" 52 | inkscape:window-y="0" 53 | inkscape:window-maximized="0" 54 | inkscape:current-layer="svg1104" /><g 55 | id="g439" 56 | transform="matrix(0.88192626,0,0,0.88192547,4694.9029,20.001364)"><linearGradient 57 | y2="12.7559" 58 | x2="-5278.1094" 59 | y1="12.7559" 60 | x1="-5295.1172" 61 | gradientUnits="userSpaceOnUse" 62 | id="SVGID_43_"><stop 63 | id="stop397" 64 | style="stop-color:#28B899" 65 | offset="0" /><stop 66 | id="stop399" 67 | style="stop-color:#1B937B" 68 | offset="1" /></linearGradient><rect 69 | style="fill:url(#SVGID_43_)" 70 | id="rect402" 71 | height="14.173" 72 | width="17.007999" 73 | y="5.6690001" 74 | x="-5295.1172" /><linearGradient 75 | y2="12.7559" 76 | x2="-5238.4248" 77 | y1="12.7559" 78 | x1="-5278.1094" 79 | gradientUnits="userSpaceOnUse" 80 | id="SVGID_44_"><stop 81 | id="stop404" 82 | style="stop-color:#03594A" 83 | offset="0" /><stop 84 | id="stop406" 85 | style="stop-color:#128570" 86 | offset="1" /></linearGradient><rect 87 | style="fill:url(#SVGID_44_)" 88 | id="rect409" 89 | height="14.173" 90 | width="39.685001" 91 | y="5.6690001" 92 | x="-5278.1089" /><linearGradient 93 | y2="34.014599" 94 | x2="-5266.7715" 95 | y1="34.014599" 96 | x1="-5295.1172" 97 | gradientUnits="userSpaceOnUse" 98 | id="SVGID_45_"><stop 99 | id="stop411" 100 | style="stop-color:#28B899" 101 | offset="0" /><stop 102 | id="stop413" 103 | style="stop-color:#1B937B" 104 | offset="1" /></linearGradient><rect 105 | style="fill:url(#SVGID_45_)" 106 | id="rect416" 107 | height="14.173" 108 | width="28.346001" 109 | y="26.927999" 110 | x="-5295.1172" /><linearGradient 111 | y2="34.013699" 112 | x2="-5238.4248" 113 | y1="34.013699" 114 | x1="-5266.7715" 115 | gradientUnits="userSpaceOnUse" 116 | id="SVGID_46_"><stop 117 | id="stop418" 118 | style="stop-color:#03594A" 119 | offset="0" /><stop 120 | id="stop420" 121 | style="stop-color:#128570" 122 | offset="1" /></linearGradient><rect 123 | style="fill:url(#SVGID_46_)" 124 | id="rect423" 125 | height="14.171" 126 | width="28.347" 127 | y="26.927999" 128 | x="-5266.771" /><linearGradient 129 | y2="55.274399" 130 | x2="-5255.4326" 131 | y1="55.274399" 132 | x1="-5295.1172" 133 | gradientUnits="userSpaceOnUse" 134 | id="SVGID_47_"><stop 135 | id="stop425" 136 | style="stop-color:#28B899" 137 | offset="0" /><stop 138 | id="stop427" 139 | style="stop-color:#1B937B" 140 | offset="1" /></linearGradient><rect 141 | style="fill:url(#SVGID_47_)" 142 | id="rect430" 143 | height="14.174" 144 | width="39.685001" 145 | y="48.188" 146 | x="-5295.1172" /><linearGradient 147 | y2="55.274399" 148 | x2="-5238.4229" 149 | y1="55.274399" 150 | x1="-5255.4326" 151 | gradientUnits="userSpaceOnUse" 152 | id="SVGID_48_"><stop 153 | id="stop432" 154 | style="stop-color:#03594A" 155 | offset="0" /><stop 156 | id="stop434" 157 | style="stop-color:#128570" 158 | offset="1" /></linearGradient><rect 159 | style="fill:url(#SVGID_48_)" 160 | id="rect437" 161 | height="14.174" 162 | width="17.01" 163 | y="48.188" 164 | x="-5255.4331" /></g></svg> ``` -------------------------------------------------------------------------------- /tests/test_use_notebook.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """ 6 | Unit tests for use_notebook tool with optional notebook_path parameter. 7 | 8 | These tests verify the notebook switching functionality when notebook_path is not provided. 9 | """ 10 | 11 | import pytest 12 | import logging 13 | from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool 14 | from jupyter_mcp_server.tools._base import ServerMode 15 | from jupyter_mcp_server.notebook_manager import NotebookManager 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_use_notebook_switching(): 20 | """Test that use_notebook can switch between already-connected notebooks""" 21 | tool = UseNotebookTool() 22 | notebook_manager = NotebookManager() 23 | 24 | # Simulate adding two notebooks manually 25 | notebook_manager.add_notebook( 26 | "notebook_a", 27 | {"id": "kernel_a"}, # Mock kernel info 28 | server_url="local", 29 | token=None, 30 | path="notebook_a.ipynb" 31 | ) 32 | 33 | notebook_manager.add_notebook( 34 | "notebook_b", 35 | {"id": "kernel_b"}, # Mock kernel info 36 | server_url="local", 37 | token=None, 38 | path="notebook_b.ipynb" 39 | ) 40 | 41 | # Set current to notebook_a 42 | notebook_manager.set_current_notebook("notebook_a") 43 | logging.debug(f"Current notebook: {notebook_manager.get_current_notebook()}") 44 | assert notebook_manager.get_current_notebook() == "notebook_a" 45 | 46 | # Test switching to notebook_b (no notebook_path provided) 47 | result = await tool.execute( 48 | mode=ServerMode.JUPYTER_SERVER, 49 | notebook_manager=notebook_manager, 50 | notebook_name="notebook_b", 51 | notebook_path=None # Key: no path provided, should just switch 52 | ) 53 | 54 | logging.debug(f"Switch result: {result}") 55 | assert "Successfully switched to notebook 'notebook_b'" in result 56 | assert notebook_manager.get_current_notebook() == "notebook_b" 57 | 58 | # Test switching back to notebook_a 59 | result = await tool.execute( 60 | mode=ServerMode.JUPYTER_SERVER, 61 | notebook_manager=notebook_manager, 62 | notebook_name="notebook_a", 63 | notebook_path=None 64 | ) 65 | 66 | logging.debug(f"Switch back result: {result}") 67 | assert "Successfully switched to notebook 'notebook_a'" in result 68 | assert notebook_manager.get_current_notebook() == "notebook_a" 69 | 70 | 71 | @pytest.mark.asyncio 72 | async def test_use_notebook_switch_to_nonexistent(): 73 | """Test error handling when switching to non-connected notebook""" 74 | tool = UseNotebookTool() 75 | notebook_manager = NotebookManager() 76 | 77 | # Add only one notebook 78 | notebook_manager.add_notebook( 79 | "notebook_a", 80 | {"id": "kernel_a"}, 81 | server_url="local", 82 | token=None, 83 | path="notebook_a.ipynb" 84 | ) 85 | 86 | # Try to switch to non-existent notebook 87 | result = await tool.execute( 88 | mode=ServerMode.JUPYTER_SERVER, 89 | notebook_manager=notebook_manager, 90 | notebook_name="notebook_c", 91 | notebook_path=None 92 | ) 93 | 94 | logging.debug(f"Non-existent notebook result: {result}") 95 | assert "not connected" in result 96 | assert "Please provide a notebook_path" in result 97 | 98 | 99 | @pytest.mark.asyncio 100 | async def test_use_notebook_with_path_still_works(): 101 | """Test that providing notebook_path still works for connecting new notebooks""" 102 | tool = UseNotebookTool() 103 | notebook_manager = NotebookManager() 104 | 105 | # This should trigger the error about missing clients (since we're not providing them) 106 | # but it verifies the code path is still intact 107 | result = await tool.execute( 108 | mode=ServerMode.JUPYTER_SERVER, 109 | notebook_manager=notebook_manager, 110 | notebook_name="new_notebook", 111 | notebook_path="new.ipynb", 112 | use_mode="connect" 113 | ) 114 | 115 | # Should fail because no contents_manager provided, but validates the logic path 116 | assert "Invalid mode or missing required clients" in result or "already using" not in result 117 | 118 | 119 | @pytest.mark.asyncio 120 | async def test_use_notebook_multiple_switches(): 121 | """Test multiple consecutive switches between notebooks""" 122 | tool = UseNotebookTool() 123 | notebook_manager = NotebookManager() 124 | 125 | # Add three notebooks 126 | for i, name in enumerate(["nb1", "nb2", "nb3"]): 127 | notebook_manager.add_notebook( 128 | name, 129 | {"id": f"kernel_{i}"}, 130 | server_url="local", 131 | token=None, 132 | path=f"{name}.ipynb" 133 | ) 134 | 135 | notebook_manager.set_current_notebook("nb1") 136 | 137 | # Perform multiple switches 138 | switches = ["nb2", "nb3", "nb1", "nb3", "nb2"] 139 | for target in switches: 140 | result = await tool.execute( 141 | mode=ServerMode.JUPYTER_SERVER, 142 | notebook_manager=notebook_manager, 143 | notebook_name=target, 144 | notebook_path=None 145 | ) 146 | assert f"Successfully switched to notebook '{target}'" in result 147 | assert notebook_manager.get_current_notebook() == target 148 | logging.debug(f"Switched to {target}") 149 | 150 | 151 | if __name__ == "__main__": 152 | # Allow running with: python tests/test_use_notebook.py 153 | pytest.main([__file__, "-v"]) 154 | ```