#
tokens: 49619/50000 98/134 files (page 1/6)
lines: on (toggle) GitHub
raw markdown copy reset
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 | [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)
 8 | 
 9 | [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](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 | [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)
 8 | 
 9 | [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](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 | [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)
 8 | 
 9 | [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](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 | [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)
  8 | 
  9 | [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](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 | [![PyPI - Version](https://img.shields.io/pypi/v/jupyter-mcp-server?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/jupyter-mcp-server)
 22 | [![Python Version](https://img.shields.io/badge/python-3.10+-blue?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
 23 | [![Docker Pulls](https://img.shields.io/docker/pulls/datalayer/jupyter-mcp-server?style=for-the-badge&logo=docker&logoColor=white&color=2496ED)](https://hub.docker.com/r/datalayer/jupyter-mcp-server)
 24 | [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue?style=for-the-badge&logo=open-source-initiative&logoColor=white)](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 | ![Jupyter MCP Server Demo](https://assets.datalayer.tech/jupyter-mcp/mcp-demo-multimodal.gif)
 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 | ![](https://assets.datalayer.tech/jupyter-mcp/windsurf.png)
 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 | ![](https://assets.datalayer.tech/jupyter-mcp/cursor.png)
 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 | ![](https://assets.datalayer.tech/jupyter-mcp/cline.png)
 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 | ![Jupyter MCP Server](https://assets.datalayer.tech/jupyter-mcp/mcp-demo-multimodal.gif)
 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&apos;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 | ![Jupyter MCP Server Demo](https://assets.datalayer.tech/jupyter-mcp/mcp-demo-multimodal.gif)
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 | 
```
Page 1/6FirstPrevNextLast