#
tokens: 49092/50000 106/134 files (page 1/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 5. Use http://codebase.md/datalayer/jupyter-mcp-server?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:
--------------------------------------------------------------------------------

```
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env
pip-log.txt
pip-delete-this-directory.txt
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.github
.mypy_cache
.pytest_cache
dev
docs
```

--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
/node_modules

# Production
/build

# Generated files
.docusaurus
.cache-loader

# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

*.lock

```

--------------------------------------------------------------------------------
/.licenserc.yaml:
--------------------------------------------------------------------------------

```yaml
header:
  license:
    content: |
      Copyright (c) 2023-2024 Datalayer, Inc.

      BSD 3-Clause License


  paths-ignore:
    - '**/*.ipynb'
    - '**/*.json'
    - '**/*.yaml'
    - '**/*.yml'
    - '**/.*'
    - 'docs/**/*'
    - 'LICENSE'

  comment: on-failure
```

--------------------------------------------------------------------------------
/docs/.yarnrc.yml:
--------------------------------------------------------------------------------

```yaml
# Copyright (c) Datalayer, Inc. https://datalayer.io
# Distributed under the terms of the MIT License.

enableImmutableInstalls: false
enableInlineBuilds: false
enableTelemetry: false
httpTimeout: 60000
nodeLinker: node-modules
npmRegistryServer: "https://registry.yarnpkg.com"
checksumBehavior: update

# This will fix the build error with @lerna/legacy-package-management
# See https://github.com/lerna/repro/pull/11
packageExtensions:
  "@lerna/legacy-package-management@*":
    dependencies:
      "@lerna/child-process": "*"
      "js-yaml": "*"
      "rimraf": "*"
    peerDependencies:
      "nx": "*"
```

--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------

```yaml
ci:
  autoupdate_schedule: monthly

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: end-of-file-fixer
      - id: check-case-conflict
      - id: check-executables-have-shebangs
      - id: requirements-txt-fixer
      - id: check-added-large-files
      - id: check-case-conflict
      - id: check-toml
      - id: check-yaml
      - id: debug-statements
      - id: forbid-new-submodules
      - id: check-builtin-literals
      - id: trailing-whitespace

  - repo: https://github.com/python-jsonschema/check-jsonschema
    rev: 0.29.4
    hooks:
      - id: check-github-workflows

  - repo: https://github.com/executablebooks/mdformat
    rev: 0.7.19
    hooks:
      - id: mdformat
        additional_dependencies:
          [mdformat-gfm, mdformat-frontmatter, mdformat-footnote]

  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: ["--fix"]
      - id: ruff-format

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
*.egg-info/
.ipynb_checkpoints

# Created by https://www.gitignore.io/api/python
# Edit at https://www.gitignore.io/?templates=python

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
dist/
downloads/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
.installed.cfg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Environment variables:
.env

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# ruff
.ruff_cache

# Pyre type checker
.pyre/

# End of https://www.gitignore.io/api/python

# OSX files
.DS_Store

# Include
!**/.*ignore
!**/.*rc
!**/.*rc.js
!**/.*rc.json
!**/.*rc.yml
!**/.*config
!*.*rc.json
!.github
!.devcontainer

untracked_notebooks/*
.jupyter_ystore
.jupyter_ystore.db
docs/.yarn/*

uv.lock
*-lock.json

```

--------------------------------------------------------------------------------
/dev/content/README.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

[![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)

```

--------------------------------------------------------------------------------
/dev/README.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

[![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)

```

--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

[![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)

# Jupyter MCP Server Docs

> Source code for the [Jupyter MCP Server Documentation](https://datalayer.io), built with [Docusaurus](https://docusaurus.io).

```bash
# Install the dependencies.
conda install yarn
yarn
```

```bash
# Local Development: This command starts a local development server and opens up a browser window.
# Most changes are reflected live without having to restart the server.
npm start
```

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

[![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)


<div align="center">

<!-- omit in toc -->
# 🪐✨ Jupyter MCP Server

**An [MCP](https://modelcontextprotocol.io) service specifically developed for AI to connect and manage Jupyter Notebooks in real-time**

*Developed by [Datalayer](https://github.com/datalayer)*

[![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)
[![Python Version](https://img.shields.io/badge/python-3.10+-blue?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![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)
[![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)


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


> 🚨 **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)

![Jupyter MCP Server Demo](https://assets.datalayer.tech/jupyter-mcp/mcp-demo-multimodal.gif)

</div>

## 📖 Table of Contents
- [Key Features](#-key-features)
- [Tools Overview](#-tools-overview)
- [Getting Started](#-getting-started)
- [Best Practices](#-best-practices)
- [Contributing](#-contributing)
- [Resources](#-resources)


## 🚀 Key Features

- ⚡ **Real-time control:** Instantly view notebook changes as they happen.
- 🔁 **Smart execution:** Automatically adjusts when a cell run fails thanks to cell output feedback.
- 🧠 **Context-aware:** Understands the entire notebook context for more relevant interactions.
- 📊 **Multimodal support:** Support different output types, including images, plots, and text.
- 📚 **Multi-notebook support:** Seamlessly switch between multiple notebooks.
- 🤝 **MCP-compatible:** Works with any MCP client, such as Claude Desktop, Cursor, Windsurf, and more.

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

## 🔧 Tools Overview

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

### Server Management

| Name | Description |
|:---|:---|
| `list_files` | Recursively list files and directories in the Jupyter server's file system. |
| `list_kernels` | List all available and running kernel sessions on the Jupyter server. |
| `assign_kernel_to_notebook` | Create a Jupyter session to connect a notebook file to a specific kernel. |

### Multi-Notebook Management

| Name | Description |
|:---|:---|
| `use_notebook` | Connect to a notebook file, create a new one, or switch between notebooks. |
| `list_notebooks` | List all notebooks available on the Jupyter server and their status  |
| `restart_notebook` | Restart the kernel for a specific managed notebook. |
| `unuse_notebook` | Disconnect from a specific notebook and release its resources. |

### Cell Operations and Execution

| Name | Description |
|:---|:---|
| `list_cells` | List basic information for all cells to provide a quick overview of notebook |
| `read_cell` | Read the full content (source and outputs) of a single cell. |
| `read_cells` | Read the full content of all cells in the notebook. |
| `insert_cell` | Insert a new code or markdown cell at a specified position. |
| `delete_cell` | Delete a cell at a specified index. |
| `overwrite_cell_source` | Overwrite the source code of an existing cell. |
| `execute_cell` | Execute a cell with timeout, it supports multimodal output including images. |
| `insert_execute_code_cell` | A convenient tool to insert a new code cell and execute it in one step. |
| `execute_ipython` | Execute IPython code directly in the kernel, including magic and shell commands. |

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

## 🏁 Getting Started

For comprehensive setup instructions—including `Streamable HTTP` transport, running as a Jupyter Server extension and advanced configuration—check out [our documentation](https://jupyter-mcp-server.datalayer.tech/). Or, get started quickly with `JupyterLab` and `STDIO` transport here below.

### 1. Set Up Your Environment

```bash
pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel
pip uninstall -y pycrdt datalayer_pycrdt
pip install datalayer_pycrdt==0.12.17
```

### 2. Start JupyterLab

```bash
# Start JupyterLab on port 8888, allowing access from any IP and setting a token
jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0
```

> [!NOTE]
> If you are running notebooks through JupyterHub instead of JupyterLab as above, you should:
>
> - Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment.
> - Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub.

### 3. Configure Your Preferred MCP Client

Next, configure your MCP client to connect to the server. We offer two primary methods—choose the one that best fits your needs:

- **📦 Using `uvx` (Recommended for Quick Start):** A lightweight and fast method using `uv`. Ideal for local development and first-time users.
- **🐳 Using `Docker` (Recommended for Production):** A containerized approach that ensures a consistent and isolated environment, perfect for production or complex setups.

<details>
<summary><b>📦 Using uvx (Quick Start)</b></summary>

First, install `uv`:
```bash
pip install uv
uv --version
# should be 0.6.14 or higher
```
See more details on [uv installation](https://docs.astral.sh/uv/getting-started/installation/).

Then, configure your client:
```json
{
  "mcpServers": {
    "jupyter": {
      "command": "uvx",
      "args": ["jupyter-mcp-server@latest"],
      "env": {
        "DOCUMENT_URL": "http://localhost:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://localhost:8888",
        "RUNTIME_TOKEN": "MY_TOKEN",
        "ALLOW_IMG_OUTPUT": "true"
      }
    }
  }
}
```

</details>

<details>
<summary><b>🐳 Using Docker (Production)</b></summary>

**On macOS and Windows:**
```json
{
  "mcpServers": {
    "jupyter": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "DOCUMENT_URL",
        "-e", "DOCUMENT_TOKEN",
        "-e", "DOCUMENT_ID",
        "-e", "RUNTIME_URL",
        "-e", "RUNTIME_TOKEN",
        "-e", "ALLOW_IMG_OUTPUT",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://host.docker.internal:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://host.docker.internal:8888",
        "RUNTIME_TOKEN": "MY_TOKEN",
        "ALLOW_IMG_OUTPUT": "true"
      }
    }
  }
}
```

**On Linux:**
```json
{
  "mcpServers": {
    "jupyter": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "DOCUMENT_URL",
        "-e", "DOCUMENT_TOKEN",
        "-e", "DOCUMENT_ID",
        "-e", "RUNTIME_URL",
        "-e", "RUNTIME_TOKEN",
        "-e", "ALLOW_IMG_OUTPUT",
        "--network=host",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://localhost:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://localhost:8888",
        "RUNTIME_TOKEN": "MY_TOKEN",
        "ALLOW_IMG_OUTPUT": "true"
      }
    }
  }
}
```

</details>

> [!TIP]
> 1. Ensure the `port` of the `DOCUMENT_URL` and `RUNTIME_URL` match those used in the `jupyter lab` command.
> 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.
> 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.
>    - **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.
>    - **Flexible:** Even if you set `DOCUMENT_ID`, the MCP client can still browse, list, switch to, or even create new notebooks at any time.

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

## ✅ Best Practices

- Interact with LLMs that supports multimodal input (like Gemini 2.5 Pro) to fully utilize advanced multimodal understanding capabilities.
- 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.
- 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.

## 🤝 Contributing

We welcome contributions of all kinds! Here are some examples:

- 🐛 Bug fixes
- 📝 Improvements to existing features
- ✨ New feature development
- 📚 Documentation improvements

For detailed instructions on how to get started with development and submit your contributions, please see our [**Contributing Guide**](CONTRIBUTING.md).

### Our Contributors

<a href="https://github.com/datalayer/jupyter-mcp-server/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=datalayer/jupyter-mcp-server" />
</a>

## 📚 Resources

Looking for blog posts, videos, or other materials about Jupyter MCP Server?

👉 Visit the [**Resources section**](https://jupyter-mcp-server.datalayer.tech/resources) in our documentation for more!

<a href="https://star-history.com/#/repos/datalayer/jupyter-mcp-server&type=Date">
  <img src="https://api.star-history.com/svg?repos=datalayer/jupyter-mcp-server&type=Date" alt="Star History Chart">
</a>

---

<div align="center">

**If this project is helpful to you, please give us a ⭐️**

Made with ❤️ by [Datalayer](https://github.com/datalayer)

</div>
```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

*   Demonstrating empathy and kindness toward other people
*   Being respectful of differing opinions, viewpoints, and experiences
*   Giving and gracefully accepting constructive feedback
*   Accepting responsibility and apologizing to those affected by our mistakes,
    and learning from the experience
*   Focusing on what is best not just for us as individuals, but for the
    overall community

Examples of unacceptable behavior include:

*   The use of sexualized language or imagery, and sexual attention or
    advances of any kind
*   Trolling, insulting or derogatory comments, and personal or political attacks
*   Public or private harassment
*   Publishing others' private information, such as a physical or email
    address, without their explicit permission
*   Other conduct which could reasonably be considered inappropriate in a
    professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].

[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

# Contributing to Jupyter MCP Server

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!

## Code of Conduct

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.

## How Can I Contribute?

We welcome contributions of all kinds, including:
- 🐛 Bug fixes
- 📝 Improvements to existing features or documentation
- ✨ New feature development

### Reporting Bugs or Suggesting Enhancements

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

- 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.
- If you're suggesting an enhancement, clearly state the enhancement you are proposing and why it would be a good addition to the project.

## Development Setup

To get started with development, you'll need to set up your environment.

1.  **Clone the repository:**
    ```bash
    git clone https://github.com/datalayer/jupyter-mcp-server
    cd jupyter-mcp-server
    ```

2.  **Install dependencies:**
    ```bash
    # Install the project in editable mode with test dependencies
    pip install -e ".[test]"
    ```

3.  **Make Some Amazing Changes!**
    ```bash
    # Make some amazing changes to the source code!
    ```

4.  **Run Tests:**
    ```bash
    make test
    ```

## (Optional) Manual Agent Testing

1.  **Build Python Package:**
    ```bash
    make build
    ```

2. **Set Up Your Environment:**
    ```bash
    pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel
    pip uninstall -y pycrdt datalayer_pycrdt
    pip install datalayer_pycrdt==0.12.17
    ```

3.  **Start Jupyter Server:**
    ```bash
    jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0
    ```

4.  **Set Up Your MCP Client:**
    We recommend using `uvx` to start the MCP server, first install `uvx` with `pip install uv`.

    ```bash
    pip install uv
    uv --version
    # should be 0.6.14 or higher
    ```

    Then, set up your MCP client with the following configuration file.

    ```json
    {
        "mcpServers": {
            "Jupyter-MCP": {
                "command": "uvx",
                "args": [
                    "--from",
                    "your/path/to/jupyter-mcp-server/dist/jupyter_mcp_server-x.x.x-py3-none-any.whl",
                    "jupyter-mcp-server"
                ],
                "env": {
                    "DOCUMENT_URL": "http://localhost:8888",
                    "DOCUMENT_TOKEN": "MY_TOKEN",
                    "DOCUMENT_ID": "notebook.ipynb",
                    "RUNTIME_URL": "http://localhost:8888",
                    "RUNTIME_TOKEN": "MY_TOKEN",
                    "ALLOW_IMG_OUTPUT": "true"
                }
            }
        }
    }
    ```

5.  **Test Your Changes:**

    You Can Test Your Changes with your favorite MCP client(e.g. Cursor, Gemini CLI, etc.).

## Pull Request Process

1.  Once you are satisfied with your changes and tests, commit your code.
2.  Push your branch to your fork and attach with detailed description of the changes you made.
3.  Open a pull request to the `main` branch of the original repository.

We look forward to your contributions!

```

--------------------------------------------------------------------------------
/docs/docs/clients/cline/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Cline"
position: 4

```

--------------------------------------------------------------------------------
/docs/docs/tools/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Tools"
position: 8

```

--------------------------------------------------------------------------------
/docs/docs/clients/cursor/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Cursor"
position: 3

```

--------------------------------------------------------------------------------
/docs/docs/clients/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Clients"
position: 4

```

--------------------------------------------------------------------------------
/docs/docs/clients/vscode/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "VS Code"
position: 2

```

--------------------------------------------------------------------------------
/docs/docs/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Overview"
position: 1

```

--------------------------------------------------------------------------------
/docs/docs/clients/windsurf/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Windsurf"
position: 5

```

--------------------------------------------------------------------------------
/docs/docs/configure/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Configure"
position: 5

```

--------------------------------------------------------------------------------
/docs/docs/releases/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Releases"
position: 11

```

--------------------------------------------------------------------------------
/docs/docs/contribute/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Contribute"
position: 9

```

--------------------------------------------------------------------------------
/docs/docs/deployment/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Deployment"
position: 2

```

--------------------------------------------------------------------------------
/docs/docs/resources/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Resources"
position: 12

```

--------------------------------------------------------------------------------
/docs/docs/clients/claude_desktop/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Claude Desktop"
position: 1

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/stdio/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "STDIO Transport"
position: 1

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Jupyter Notebooks"
position: 1

```

--------------------------------------------------------------------------------
/docs/docs/deployment/datalayer/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Datalayer Notebooks"
position: 2

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/streamable-http/_category_.yaml:
--------------------------------------------------------------------------------

```yaml
label: "Streamable HTTP Transport"
position: 2

```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

```

--------------------------------------------------------------------------------
/docs/docs/deployment/datalayer/streamable-http/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Streamable HTTP Transport

:::warning
Documentation under construction.
:::

```

--------------------------------------------------------------------------------
/jupyter-config/jupyter_notebook_config/jupyter_mcp_server.json:
--------------------------------------------------------------------------------

```json
{
  "ServerApp": {
    "nbserver_extensions": {
      "jupyter_mcp_server": true
    }
  }
}

```

--------------------------------------------------------------------------------
/jupyter-config/jupyter_server_config.d/jupyter_mcp_server.json:
--------------------------------------------------------------------------------

```json
{
  "ServerApp": {
    "jpserver_extensions": {
      "jupyter_mcp_server": true
    }
  }
}

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

# Changelog

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/__version__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Jupyter MCP Server."""

__version__ = "0.16.0"

```

--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------

```
; Copyright (c) 2023-2024 Datalayer, Inc.
;
; BSD 3-Clause License

[pytest]
addopts = -rqA
log_cli = true
log_level = INFO

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/protocol/__init__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""MCP Protocol implementation for Jupyter Server extension"""

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/backends/__init__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Backend implementations for notebook and kernel operations"""

```

--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------

```javascript
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

module.exports = {
  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/__main__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

from jupyter_mcp_server.server import server

if __name__ == "__main__":
    """Start the Jupyter MCP Server."""
    server()

```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
{
    "python-envs.pythonProjects": [
        {
            "path": "",
            "envManager": "ms-python.python:conda",
            "packageManager": "ms-python.python:conda"
        }
    ]
}
```

--------------------------------------------------------------------------------
/docs/src/pages/markdown-page.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

---
title: Markdown page example
---

# Markdown page example

You don't need React to write simple standalone pages.

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/__init__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Jupyter MCP Server."""

from jupyter_mcp_server.jupyter_extension.extension import _jupyter_server_extension_points


__all__ = ["_jupyter_server_extension_points"]

```

--------------------------------------------------------------------------------
/.github/workflows/lint.sh:
--------------------------------------------------------------------------------

```bash
#!/usr/bin/env bash
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

pip install -e ".[lint,typing]"
mypy --install-types --non-interactive .
ruff check .
mdformat --check *.md
pipx run 'validate-pyproject[all]' pyproject.toml

```

--------------------------------------------------------------------------------
/docs/src/components/HomepageProducts.module.css:
--------------------------------------------------------------------------------

```css
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

/* stylelint-disable docusaurus/copyright-header */

.product {
  display: flex;
  align-items: center;
  padding: 2rem 0;
  width: 100%;
}

.productSvg {
  height: 200px;
  width: 200px;
}

```

--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures.module.css:
--------------------------------------------------------------------------------

```css
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

/* stylelint-disable docusaurus/copyright-header */

.features {
  display: flex;
  align-items: center;
  padding: 2rem 0;
  width: 100%;
}

.featureSvg {
  height: 200px;
  width: 200px;
}

```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      actions:
        patterns:
          - "*"
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      pip:
        patterns:
          - "*"

```

--------------------------------------------------------------------------------
/docs/src/theme/CustomDocItem.tsx:
--------------------------------------------------------------------------------

```typescript
import React from "react";
import { ThemeProvider } from '@primer/react-brand';
import DocItem from "@theme/DocItem";

import '@primer/react-brand/lib/css/main.css'

export const CustomDocItem = (props: any) => {
  return (
    <>
      <ThemeProvider>
        <DocItem {...props}/>
      </ThemeProvider>
    </>
  )
}

export default CustomDocItem;

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

FROM python:3.10-slim

WORKDIR /app

COPY pyproject.toml LICENSE README.md ./
COPY jupyter_mcp_server/ jupyter_mcp_server/
COPY jupyter-config/ jupyter-config/

RUN pip install --no-cache-dir -e . && \
    pip uninstall -y pycrdt datalayer_pycrdt && \
    pip install --no-cache-dir datalayer_pycrdt==0.12.17

EXPOSE 4040

ENTRYPOINT ["python", "-m", "jupyter_mcp_server"]

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/__init__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Jupyter to MCP Adapter Package

This package provides the adapter layer to expose MCP server tools as a Jupyter Server extension.
It supports dual-mode operation: standalone MCP server and embedded Jupyter server extension.
"""

from jupyter_mcp_server.jupyter_extension.context import ServerContext, get_server_context

__all__ = ["ServerContext", "get_server_context"]

```

--------------------------------------------------------------------------------
/docs/docs/clients/windsurf/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Windsurf

![](https://assets.datalayer.tech/jupyter-mcp/windsurf.png)

## Install Windsurf

Install the Windsurf app from the [Windsurf website](https://windsurf.com/download).

## Configure Jupyter MCP Server

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

```

--------------------------------------------------------------------------------
/docs/docs/clients/cursor/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Cursor

![](https://assets.datalayer.tech/jupyter-mcp/cursor.png)

## Install Cursor

Install the Cursor app from the [Cursor website](https://www.cursor.com/downloads).

## Configure Jupyter MCP Server

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

```

--------------------------------------------------------------------------------
/docs/src/pages/testimonials.tsx:
--------------------------------------------------------------------------------

```typescript
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

import React from 'react';
import Layout from '@theme/Layout';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import HomepageFeatures from '../components/HomepageFeatures';

export default function Home() {
  const {siteConfig} = useDocusaurusContext();
  return (
    <Layout
      title={`${siteConfig.title}`}
      description="Datalayer, cloud native Jupyter">
      <main>
        <HomepageFeatures />
      </main>
    </Layout>
  );
}

```

--------------------------------------------------------------------------------
/docs/docs/contribute/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Contribute

## Develop

Clone the repository and install the dependencies.

```bash
git clone https://github.com/datalayer/jupyter-mcp-server
cd jupyter-mcp-server
pip install -e .
```

Build the Docker image from source.

```bash
make build-docker
```

## Contribute

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

Your contributions help us improve the project and make it more useful for everyone!

```

--------------------------------------------------------------------------------
/docs/docs/clients/cline/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Cline

![](https://assets.datalayer.tech/jupyter-mcp/cline.png)

## Install Cline VS Code extension

Install the Cline VS Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev).

## Configure Jupyter MCP Server

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

```

--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------

```javascript
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

/**
 * Creating a sidebar enables you to:
 - create an ordered group of docs
 - render a sidebar for each doc of that group
 - provide next/previous navigation

 The sidebars can be generated from the filesystem, or explicitly defined here.

 Create as many sidebars as you want.
 */

// @ts-check

/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
  jupyterMCPServerSidebar: [
    {
      type: 'autogenerated',
      dirName: '.',
    },
  ]
};

module.exports = sidebars;

```

--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------

```css
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

/* stylelint-disable docusaurus/copyright-header */

/**
 * CSS files with the .module.css suffix will be treated as CSS modules
 * and scoped locally.
 */

.heroBanner {
  padding: 4rem 0;
  text-align: center;
  position: relative;
  overflow: hidden;
}

@media screen and (max-width: 966px) {
  .heroBanner {
    padding: 2rem;
  }
}

.buttons {
  display: flex;
  align-items: center;
  justify-content: center;
}

.tag {
  font-size: small;
  padding: 4px;
  border-radius: 5px;
  border-width: thick;
  border-color: red;
  background: orange;
}

```

--------------------------------------------------------------------------------
/docs/docs/clients/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Clients

We have tested and validated the Jupyter MCP Server with the following clients:

- [Claude Desktop](./claude_desktop)
- [VS Code](./vscode)
- [Cursor](./cursor)
- [Cline](./cline)
- [Windsurf](./windsurf)

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.

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.

```

--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------

```yaml
name: Test

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

defaults:
  run:
    shell: bash -eux {0}

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.10", "3.13"]

    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install the extension
        run: |
          python -m pip install ".[test]"
          pip uninstall -y pycrdt datalayer_pycrdt
          pip install datalayer_pycrdt==0.12.17

      - name: Test the extension
        run: |
          make test-mcp-server
          make test-jupyter-server

```

--------------------------------------------------------------------------------
/.vscode/mcp.json:
--------------------------------------------------------------------------------

```json
{
  "servers": {
    // https://github.com/github/github-mcp-server
    "Github": {
      "url": "https://api.githubcopilot.com/mcp"
    },
    // This configuration is for Docker on Linux, read https://jupyter-mcp-server.datalayer.tech/clients/
    "DatalayerJupyter": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "DOCUMENT_URL",
        "-e",
        "DOCUMENT_TOKEN",
        "-e",
        "DOCUMENT_ID",
        "-e",
        "RUNTIME_URL",
        "-e",
        "RUNTIME_TOKEN",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://host.docker.internal:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://host.docker.internal:8888",
        "RUNTIME_TOKEN": "MY_TOKEN"
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - serverUrl
      - token
      - notebookPath
    properties:
      serverUrl:
        type: string
        description: The URL of the JupyterLab server that the MCP will connect to.
      token:
        type: string
        description: The token for authenticating with the JupyterLab server.
      notebookPath:
        type: string
        description: The path to the Jupyter notebook to work with.
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (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'] })

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/index.mdx:
--------------------------------------------------------------------------------

```markdown

# Jupyter Notebooks

This guide will help you set up a Jupyter MCP Server to connect your preferred MCP client to a JupyterLab instance.
The Jupyter MCP Server acts as a bridge between the MCP client and the JupyterLab server, allowing you to interact with Jupyter notebooks seamlessly.

You can customize the setup further based on your requirements. Refer to the [server configuration](/configure) for more details on the possible configurations.

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.
For more details on the different transports, refer to the official MCP documentation [here](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports).

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

```

--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@datalayer/jupyter-mcp-server-docs",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "docusaurus": "docusaurus",
    "start": "docusaurus start",
    "build": "docusaurus build",
    "swizzle": "docusaurus swizzle",
    "deploy": "docusaurus deploy",
    "clear": "docusaurus clear",
    "serve": "docusaurus serve",
    "write-translations": "docusaurus write-translations",
    "write-heading-ids": "docusaurus write-heading-ids"
  },
  "dependencies": {
    "@datalayer/icons-react": "^1.0.0",
    "@datalayer/primer-addons": "^1.0.3",
    "@docusaurus/core": "^3.5.2",
    "@docusaurus/preset-classic": "^3.5.2",
    "@docusaurus/theme-live-codeblock": "^3.5.2",
    "@docusaurus/theme-mermaid": "^3.5.2",
    "@mdx-js/react": "^3.0.1",
    "@primer/react-brand": "^0.58.0",
    "clsx": "^2.1.1",
    "docusaurus-lunr-search": "^3.5.0",
    "react": "18.3.1",
    "react-calendly": "^4.1.0",
    "react-dom": "18.3.1",
    "react-modal-image": "^2.6.0"
  },
  "browserslist": {
    "production": [
      ">0.5%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

```

--------------------------------------------------------------------------------
/docs/docs/clients/claude_desktop/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Claude Desktop

![Jupyter MCP Server](https://assets.datalayer.tech/jupyter-mcp/mcp-demo-multimodal.gif)

## Install Claude Desktop

Claude Desktop can be downloaded [from this page](https://claude.ai/download) for macOS and Windows.

For Linux, we had success using this [UNOFFICIAL build script based on nix](https://github.com/k3d3/claude-desktop-linux-flake)

```bash
# ⚠️ UNOFFICIAL
# You can also run `make claude-linux`
NIXPKGS_ALLOW_UNFREE=1 nix run github:k3d3/claude-desktop-linux-flake \
  --impure \
  --extra-experimental-features flakes \
  --extra-experimental-features nix-command
```

## Configure Jupyter MCP Server

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

**📺 Watch the setup demo**

<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 />

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/env.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Environment Configuration Management Module

This module manages environment variables for multimodal output support.
Following the same pattern as other environment variables in the project.
"""

import os


def _get_env_bool(env_name: str, default_value: bool = True) -> bool:
    """
    Get boolean value from environment variable, supporting multiple formats.
    
    Args:
        env_name: Environment variable name
        default_value: Default value
        
    Returns:
        bool: Boolean value
    """
    env_value = os.getenv(env_name)
    if env_value is None:
        return default_value
    
    # Supported true value formats
    true_values = {'true', '1', 'yes', 'on', 'enable', 'enabled'}
    # Supported false value formats  
    false_values = {'false', '0', 'no', 'off', 'disable', 'disabled'}
    
    env_value_lower = env_value.lower().strip()
    
    if env_value_lower in true_values:
        return True
    elif env_value_lower in false_values:
        return False
    else:
        return default_value


# Multimodal Output Configuration
# Environment variable controls whether to return actual image content or text placeholder
ALLOW_IMG_OUTPUT: bool = _get_env_bool("ALLOW_IMG_OUTPUT", True)

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/models.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

from typing import Optional, Literal, Union
from pydantic import BaseModel
from mcp.types import ImageContent
from jupyter_mcp_server.utils import safe_extract_outputs, normalize_cell_source


class DocumentRuntime(BaseModel):
    provider: str
    document_url: str
    document_id: str
    document_token: str
    runtime_url: str
    runtime_id: str
    runtime_token: str


class CellInfo(BaseModel):
    """Notebook cell information as returned by the MCP server"""

    index: int
    type: Literal["unknown", "code", "markdown"]
    source: list[str]
    outputs: Optional[list[Union[str, ImageContent]]]

    @classmethod
    def from_cell(cls, cell_index: int, cell: dict):
        """Extract cell info (create a CellInfo object) from an index and a Notebook cell"""
        outputs = None
        type = cell.get("cell_type", "unknown")
        if type == "code":
            try:
                outputs = cell.get("outputs", [])
                outputs = safe_extract_outputs(outputs)
            except Exception as e:
                outputs = [f"[Error reading outputs: {str(e)}]"]
        
        # Properly normalize the cell source to a list of lines
        source = normalize_cell_source(cell.get("source", ""))
        
        return cls(
            index=cell_index, type=type, source=source, outputs=outputs
        )

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/streamable-http/jupyter-extension/index.mdx:
--------------------------------------------------------------------------------

```markdown
# As a Jupyter Server Extension

## 1. Start JupyterLab and the MCP Server

### Environment setup

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

```bash
pip install "jupyter-mcp-server>=0.15.0" "jupyterlab==4.4.1" "jupyter-collaboration==4.0.2" "ipykernel"
pip uninstall -y pycrdt datalayer_pycrdt
pip install datalayer_pycrdt==0.12.17
```

### JupyterLab and MCP start

Then, start JupyterLab with the following command.

```bash
jupyter lab \
	  --JupyterMCPServerExtensionApp.document_url local \
	  --JupyterMCPServerExtensionApp.runtime_url local \
	  --JupyterMCPServerExtensionApp.document_id notebook.ipynb \
	  --JupyterMCPServerExtensionApp.start_new_runtime True \
	  --ServerApp.disable_check_xsrf True \
	  --IdentityProvider.token MY_TOKEN \
	  --ServerApp.root_dir ./dev/content \
	  --port 4040
  ```

You can also run `start-jupyter-server-extension` if you cloned the repository.

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.

## 2. Configure your MCP Client

Use the following configuration for you MCP client to connect to a running Jupyter MCP Server.

```json
{
  "mcpServers": {
    "jupyter": {
        "command": "npx",
        "args": ["mcp-remote", "http://127.0.0.1:4040/mcp"]
    }
  }
}
```

```

--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures.js:
--------------------------------------------------------------------------------

```javascript
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

import React from 'react';
import clsx from 'clsx';
import styles from './HomepageFeatures.module.css';

const FeatureList = [
/*
  {
    title: 'Easy to Use',
    Svg: require('../../static/img/feature_1.svg').default,
    description: (
      <>
        Datalayer was designed from the ground up to be easily installed and
        used to get your data analysis up and running quickly.
      </>
    ),
  },
  {
    title: 'Focus on What Matters',
    Svg: require('../../static/img/feature_2.svg').default,
    description: (
      <>
        Datalayer lets you focus on your work, and we&apos;ll do the chores.
      </>
    ),
  },
  {
    title: 'Powered by Open Source',
    Svg: require('../../static/img/feature_3.svg').default,
    description: (
      <>
        Extend or customize your platform to your needs.
      </>
    ),
  },
*/
];

function Feature({Svg, title, description}) {
  return (
    <div className={clsx('col col--4')}>
      <div className="text--center">
        <Svg className={styles.featureSvg} alt={title} />
      </div>
      <div className="text--center padding-horiz--md">
        <h3>{title}</h3>
        <p>{description}</p>
      </div>
    </div>
  );
}

export default function HomepageFeatures() {
  return (
    <section className={styles.features}>
      <div className="container">
        <div className="row">
          {FeatureList.map((props, idx) => (
            <Feature key={idx} {...props} />
          ))}
        </div>
      </div>
    </section>
  );
}

```

--------------------------------------------------------------------------------
/.github/workflows/prep-release.yml:
--------------------------------------------------------------------------------

```yaml
name: "Step 1: Prep Release"
on:
  workflow_dispatch:
    inputs:
      version_spec:
        description: "New Version Specifier"
        default: "next"
        required: false
      branch:
        description: "The branch to target"
        required: false
      post_version_spec:
        description: "Post Version Specifier"
        required: false
      # silent:
      #   description: "Set a placeholder in the changelog and don't publish the release."
      #   required: false
      #   type: boolean
      since:
        description: "Use PRs with activity since this date or git reference"
        required: false
      since_last_stable:
        description: "Use PRs with activity since the last stable git tag"
        required: false
        type: boolean
jobs:
  prep_release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

      - name: Prep Release
        id: prep-release
        uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          version_spec: ${{ github.event.inputs.version_spec }}
          # silent: ${{ github.event.inputs.silent }}
          post_version_spec: ${{ github.event.inputs.post_version_spec }}
          branch: ${{ github.event.inputs.branch }}
          since: ${{ github.event.inputs.since }}
          since_last_stable: ${{ github.event.inputs.since_last_stable }}

      - name: "** Next Step **"
        run: |
          echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"

```

--------------------------------------------------------------------------------
/.github/workflows/fix-license-header.yml:
--------------------------------------------------------------------------------

```yaml
name: Fix License Headers

on:
  pull_request_target:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true
    
jobs:
  header-license-fix:
    runs-on: ubuntu-latest

    permissions:
      contents: write
      pull-requests: write

    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Checkout the branch from the PR that triggered the job
        run: gh pr checkout ${{ github.event.pull_request.number }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Fix License Header
        # pin to include https://github.com/apache/skywalking-eyes/pull/168
        uses: apache/skywalking-eyes/header@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1
        with:
          mode: fix

      - name: List files changed
        id: files-changed
        shell: bash -l {0}
        run: |
          set -ex
          export CHANGES=$(git status --porcelain | tee /tmp/modified.log | wc -l)
          cat /tmp/modified.log

          echo "N_CHANGES=${CHANGES}" >> $GITHUB_OUTPUT

          git diff

      - name: Commit any changes
        if: steps.files-changed.outputs.N_CHANGES != '0'
        shell: bash -l {0}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          git pull --no-tags

          git add *
          git commit -m "Automatic application of license header"

          git config push.default upstream
          git push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

```

--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Simple test script to verify the configuration system works correctly.
"""

from jupyter_mcp_server.config import get_config, set_config, reset_config

def test_config():
    """Test the configuration singleton."""
    print("Testing Jupyter MCP Configuration System")
    print("=" * 50)
    
    # Test default configuration
    config = get_config()
    print(f"Default runtime_url: {config.runtime_url}")
    print(f"Default document_id: {config.document_id}")
    print(f"Default provider: {config.provider}")
    
    # Test setting configuration
    new_config = set_config(
        runtime_url="http://localhost:9999",
        document_id="test_notebooks.ipynb",
        provider="datalayer",
        runtime_token="test_token"
    )
    
    print(f"\nUpdated runtime_url: {new_config.runtime_url}")
    print(f"Updated document_id: {new_config.document_id}")
    print(f"Updated provider: {new_config.provider}")
    print(f"Updated runtime_token: {'***' if new_config.runtime_token else 'None'}")
    
    # Test that singleton works - getting config again should return same values
    config2 = get_config()
    print(f"\nSingleton test - runtime_url: {config2.runtime_url}")
    print(f"Singleton test - document_id: {config2.document_id}")
    
    # Test reset
    reset_config()
    config3 = get_config()
    print(f"\nAfter reset - runtime_url: {config3.runtime_url}")
    print(f"After reset - document_id: {config3.document_id}")
    print(f"After reset - provider: {config3.provider}")
    
    print("\n✅ Configuration system test completed successfully!")

if __name__ == "__main__":
    test_config()

```

--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------

```markdown
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

# Making a new release of jupyter_mcp_server

The extension can be published to `PyPI` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser).

## Manual release

### Python package

This repository can be distributed as Python
package. All of the Python
packaging instructions in the `pyproject.toml` file to wrap your extension in a
Python package. Before generating a package, we first need to install `build`.

```bash
pip install build twine
```

To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do:

```bash
python -m build
```

Then to upload the package to PyPI, do:

```bash
twine upload dist/*
```

## Automated releases with the Jupyter Releaser

> [!NOTE]
> The extension repository is compatible with the Jupyter Releaser. But
> the GitHub repository and PyPI may need to be properly set up. Please
> follow the instructions of the Jupyter Releaser [checklist](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html).

Here is a summary of the steps to cut a new release:

- Go to the Actions panel
- Run the "Step 1: Prep Release" workflow
- Check the draft changelog
- Run the "Step 2: Publish Release" workflow

> [!NOTE]
> Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html)
> for more information.

## Publishing to `conda-forge`

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

Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically.

```

--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------

```yaml
name: "Step 2: Publish Release"
on:
  workflow_dispatch:
    inputs:
      branch:
        description: "The target branch"
        required: false
      release_url:
        description: "The URL of the draft GitHub release"
        required: false
      steps_to_skip:
        description: "Comma separated list of steps to skip"
        required: false

jobs:
  publish_release:
    runs-on: ubuntu-latest
    environment: release
    permissions:
      id-token: write
    steps:
      - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

      - uses: actions/create-github-app-token@v2
        id: app-token
        with:
          app-id: ${{ vars.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}

      - name: Populate Release
        id: populate-release
        uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
        with:
          token: ${{ steps.app-token.outputs.token }}
          branch: ${{ github.event.inputs.branch }}
          release_url: ${{ github.event.inputs.release_url }}
          steps_to_skip: ${{ github.event.inputs.steps_to_skip }}

      - name: Finalize Release
        id: finalize-release
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
        with:
          token: ${{ steps.app-token.outputs.token }}
          release_url: ${{ steps.populate-release.outputs.release_url }}

      - name: "** Next Step **"
        if: ${{ success() }}
        run: |
          echo "Verify the final release"
          echo ${{ steps.finalize-release.outputs.release_url }}

      - name: "** Failure Message **"
        if: ${{ failure() }}
        run: |
          echo "Failed to Publish the Draft Release Url:"
          echo ${{ steps.populate-release.outputs.release_url }}

```

--------------------------------------------------------------------------------
/docs/docs/releases/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Releases

## 0.16.x - 13 Oct 2025

- [Merge the three execute tools into a single unified tool](https://github.com/datalayer/jupyter-mcp-server/pull/111)

## 0.15.x - 08 Oct 2025

- [Run as Jupyter Server Extension + Tool registry + Use tool](https://github.com/datalayer/jupyter-mcp-server/pull/95)
- [simplify tool implementations](https://github.com/datalayer/jupyter-mcp-server/pull/101)
- [add uvx as alternative MCP server startup method](https://github.com/datalayer/jupyter-mcp-server/pull/101)
- [document as a Jupyter Extension](https://github.com/datalayer/jupyter-mcp-server/pull/101)
- Fix Minor Bugs: [#108](https://github.com/datalayer/jupyter-mcp-server/pull/108),[#110](https://github.com/datalayer/jupyter-mcp-server/pull/110)

## 0.14.0 - 03 Oct 2025

- [Additional Tools & Bug fixes](https://github.com/datalayer/jupyter-mcp-server/pull/93).
- [Execute IPython](https://github.com/datalayer/jupyter-mcp-server/pull/90).
- [Multi notebook management](https://github.com/datalayer/jupyter-mcp-server/pull/88).

## 0.13.0 - 25 Sep 2025

- [Add multimodal output support for Jupyter cell execution](https://github.com/datalayer/jupyter-mcp-server/pull/75).
- [Unify cell insertion functionality](https://github.com/datalayer/jupyter-mcp-server/pull/73).

## 0.11.0 - 01 Aug 2025

- [Rename room to document](https://github.com/datalayer/jupyter-mcp-server/pull/35).

## 0.10.2 - 17 Jul 2025

- [Tools docstring improvements](https://github.com/datalayer/jupyter-mcp-server/pull/30).

## 0.10.1 - 11 Jul 2025

- [CORS Support](https://github.com/datalayer/jupyter-mcp-server/pull/29).

## 0.10.0 - 07 Jul 2025

- More [fixes](https://github.com/datalayer/jupyter-mcp-server/pull/28) issues for nbclient stop.

## 0.9.0 - 02 Jul 2025

- Fix issues with `nbmodel` stops.

## 0.6.0 - 01 Jul 2025

- Configuration change, see details on the [clients page](/clients) and [server configuration](/configure).

```

--------------------------------------------------------------------------------
/docs/src/components/HomepageProducts.js:
--------------------------------------------------------------------------------

```javascript
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

import React from 'react';
import clsx from 'clsx';
import styles from './HomepageProducts.module.css';

const ProductList = [
/*
  {
    title: 'Jupyter MCP Server',
    Svg: require('../../static/img/product_1.svg').default,
    description: (
      <>
        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.
      </>
    ),
  },
  {
    title: 'Jupyter',
    Svg: require('../../static/img/product_2.svg').default,
    description: (
      <>
        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...
      </>
    ),
  },
  {
    title: 'Sharebook',
    Svg: require('../../static/img/product_3.svg').default,
    description: (
      <>
        For a truly collaborative and accessible notebook, try Sharebook, a better better literate notebook, with built-in collaboration, accessibility...
      </>
    ),
  },
*/
];

function Product({Svg, title, description}) {
  return (
    <div className={clsx('col col--4')}>
      <div className="text--center">
        <Svg className={styles.productSvg} alt={title} />
      </div>
      <div className="text--center padding-horiz--md">
        <h3>{title}</h3>
        <p>{description}</p>
      </div>
    </div>
  );
}

export default function HomepageProducts() {
  return (
    <section className={styles.Products}>
      <div className="container">
        <div className="row">
          {ProductList.map((props, idx) => (
            <Product key={idx} {...props} />
          ))}
        </div>
      </div>
    </section>
  );
}

```

--------------------------------------------------------------------------------
/docs/docs/deployment/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Deployment

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.

Navigate to the relevant section based on your needs:
- ***Jupyter Notebooks***: If you want to interact with notebooks in JupyterLab/JupyterHub.
- ***Datalayer Notebooks***: If you want to interact with notebooks hosted on [Datalayer](https://datalayer.ai).
- ***STDIO Transport***: If you want to set up the MCP Server using standard input/output (STDIO) transport.
- ***Streamable HTTP Transport***: If you want to set up the MCP Server using Streamable HTTP transport.
- ***As a Standalone Server***: If you want to set up the MCP Server as a Standalone Server.
- ***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.

You can find below diagrams illustrating the different configurations.

## As a Standalone Server

The following diagram illustrates how **Jupyter MCP Server** connects to a **Jupyter server** or **Datalayer** and communicates with an MCP client.

<img
    src="https://assets.datalayer.tech/jupyter-mcp/diagram.png"
    alt="Jupyter MCP Diagram"
    style={{ width: "700px", marginBottom: "2rem" }}
/>

## As a Jupyter Server Extension

The following diagram illustrates how **Jupyter MCP Server** runs as an extension inside a **Jupyter server** and communicates with an MCP client.
In this configuration, you don't need to run a separate MCP server. It will start automatically when you start your Jupyter server.
Note that only **Streamable HTTP** transport is supported in this configuration.

<img
    src="https://assets.datalayer.tech/jupyter-mcp/diagram-jupyter-extension.png"
    alt="Jupyter MCP Diagram Jupyter Extension"
    style={{ width: "700px", marginBottom: "2rem" }}
/>

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/_base.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Base classes and enums for MCP tools."""

from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, Optional

from jupyter_server_api import JupyterServerClient
from jupyter_kernel_client import KernelClient


class ServerMode(str, Enum):
    """Enum to indicate which server mode the tool is running in."""
    MCP_SERVER = "mcp_server"
    JUPYTER_SERVER = "jupyter_server"


class BaseTool(ABC):
    """Abstract base class for all MCP tools.
    
    Each tool must implement the execute method which handles both
    MCP_SERVER mode (using HTTP clients) and JUPYTER_SERVER mode
    (using direct API access to serverapp managers).
    """
    
    def __init__(self):
        """Initialize the tool."""
        pass
    
    @abstractmethod
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[KernelClient] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        **kwargs
    ) -> Any:
        """Execute the tool logic.
        
        Args:
            mode: ServerMode indicating MCP_SERVER or JUPYTER_SERVER
            server_client: JupyterServerClient for HTTP access (MCP_SERVER mode)
            kernel_client: KernelClient for kernel HTTP access (MCP_SERVER mode)
            contents_manager: Direct access to contents manager (JUPYTER_SERVER mode)
            kernel_manager: Direct access to kernel manager (JUPYTER_SERVER mode)
            kernel_spec_manager: Direct access to kernel spec manager (JUPYTER_SERVER mode)
            **kwargs: Tool-specific parameters
            
        Returns:
            Tool execution result (type varies by tool)
        """
        pass
    
    @property
    @abstractmethod
    def name(self) -> str:
        """Return the tool name."""
        pass
    
    @property
    @abstractmethod
    def description(self) -> str:
        """Return the tool description."""
        pass

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/server_modes.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Utility functions for detecting and handling server mode."""

from typing import Tuple, Optional, Any
from jupyter_server_api import JupyterServerClient
from jupyter_mcp_server.config import get_config


def get_server_mode_and_clients() -> Tuple[str, Optional[JupyterServerClient], Optional[Any], Optional[Any], Optional[Any]]:
    """Determine server mode and get appropriate clients/managers.
    
    Returns:
        Tuple of (mode, server_client, contents_manager, kernel_manager, kernel_spec_manager)
        - mode: "local" if using local API, "http" if using HTTP clients
        - server_client: JupyterServerClient or None
        - contents_manager: Local contents manager or None
        - kernel_manager: Local kernel manager or None  
        - kernel_spec_manager: Local kernel spec manager or None
    """
    config = get_config()
    
    # Check if we should use local API
    try:
        from jupyter_mcp_server.jupyter_extension.context import get_server_context
        context = get_server_context()
        
        if context.is_local_document() and context.get_contents_manager() is not None:
            # JUPYTER_SERVER mode with local API access
            return (
                "local",
                None,
                context.get_contents_manager(),
                context.get_kernel_manager(),
                context.get_kernel_spec_manager()
            )
    except (ImportError, Exception):
        # Context not available or error, fall through to HTTP mode
        pass
    
    # MCP_SERVER mode with HTTP clients
    server_client = JupyterServerClient(
        base_url=config.runtime_url,
        token=config.runtime_token
    )
    
    return ("http", server_client, None, None, None)


def is_local_mode() -> bool:
    """Check if running in local API mode.
    
    Returns:
        True if using local serverapp API, False if using HTTP clients
    """
    try:
        from jupyter_mcp_server.jupyter_extension.context import get_server_context
        context = get_server_context()
        return context.is_local_document() and context.get_contents_manager() is not None
    except (ImportError, Exception):
        return False

```

--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------

```yaml
name: Build

on:
  push:
    branches: ["main"]
  pull_request:

defaults:
  run:
    shell: bash -eux {0}

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.10", "3.13"]

    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}

      - name: Base Setup
        uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

      - name: Install the extension
        run: |
          python -m pip install ".[test]"

      - name: Build the extension
        run: |
          pip install build
          python -m build --sdist
          cp dist/*.tar.gz jupyter_mcp_server.tar.gz
          pip uninstall -y "jupyter_mcp_server"
          rm -rf "jupyter_mcp_server"

      - uses: actions/upload-artifact@v4
        if: startsWith(matrix.os, 'ubuntu')
        with:
          name: jupyter_mcp_server-sdist-${{ matrix.python-version }}
          path: jupyter_mcp_server.tar.gz

  check_links:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
      - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1

  test_lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
      - name: Run Linters
        run: |
          bash ./.github/workflows/lint.sh

  test_sdist:
    needs: build
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.13"]

    steps:
      - name: Checkout
        uses: actions/checkout@v5
      - name: Install Python
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          architecture: "x64"
      - uses: actions/download-artifact@v5
        with:
          name: jupyter_mcp_server-sdist-${{ matrix.python-version }}
      - name: Install and Test
        run: |
          pip install jupyter_mcp_server.tar.gz
          pip list 2>&1 | grep -ie "jupyter_mcp_server"
          python -c "import jupyter_mcp_server"

```

--------------------------------------------------------------------------------
/docs/docs/index.mdx:
--------------------------------------------------------------------------------

```markdown
---
title: Overview
sidebar_position: 1
hide_table_of_contents: false
slug: /
---

# Overview

:::info

**🚨 NEW IN 0.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)

:::

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

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.

Key features include:

- ⚡ **Real-time control:** Instantly view notebook changes as they happen.
- 🔁 **Smart execution:** Automatically adjusts when a cell run fails thanks to cell output feedback.
- 🧠 **Context-aware:** Understands the entire notebook context for more relevant interactions.
- 📊 **Multimodal support:** Support different output types, including images, plots, and text.
- 📁 **Multi-notebook support:** Seamlessly switch between multiple notebooks.
- 🤝 **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.

To use Jupyter MCP Server, you first need to decide which setup fits your needs:
- ***Editor***: Do you want to interact with notebooks in Jupyter or with Datalayer hosted Notebooks?
- ***MCP Transport***: Do you want to set up the MCP Server using standard input/output (STDIO) transport or Streamable HTTP transport?
- ***MCP Server Location***: Do you want to set up the MCP Server as a Standalone Server or as a Jupyter Server Extension?

Navigate to the relevant section in the [Deployment](./deployment) page to get started based on your needs.

Looking for blog posts, videos or other resources related to Jupyter MCP Server?  <br />
👉 Check out the [Resources](./resources) section.

🧰 Dive into the [Tools section](./tools) to understand the tools powering the server.

![Jupyter MCP Server Demo](https://assets.datalayer.tech/jupyter-mcp/mcp-demo-multimodal.gif)

```

--------------------------------------------------------------------------------
/docs/docs/resources/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Resources

## Articles & Blog Posts

- [HuggingFace Blog - How to Install and Use Jupyter MCP Server](https://huggingface.co/blog/lynn-mikami/jupyter-mcp-server)
- [Analytics Vidhya - How to Use Jupyter MCP Server?](https://www.analyticsvidhya.com/blog/2025/05/jupyter-mcp-server/)
- [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)
- [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)
- [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)
- [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)
- [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)
- [Medium Sreekar Kashyap - MCP Servers + Ollama](https://medium.com/@sreekarkashyap7/mcp-servers-ollama-fad991461e88)
- [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)

## Videos

- [Data Science in your pocket - Jupyter MCP : AI for Jupyter Notebooks](https://www.youtube.com/watch?v=qkoEsqiWDOU)
- [Datalayer - How to Set Up the Jupyter MCP Server (via Claude Desktop)](https://www.youtube.com/watch?v=nPllCQxtaxQ)

## MCP Directories

- [Model Context Protocol Servers](https://github.com/modelcontextprotocol/servers)
- [Awesome MCP Servers](https://github.com/punkpeye/awesome-mcp-servers)

## MCP Registries

- [MCP.so](https://mcp.so/server/Integrating-the-Jupyter-server-with-claude-desktop-uisng-the-powerful-model-context-protocol/harshitha-8)
- [MCP Market](https://mcpmarket.com/server/jupyter)
- [MCP Servers Finder](https://www.mcpserverfinder.com/servers/ihrpr/mcp-server-jupyter)
- [Pulse MCP](https://www.pulsemcp.com/servers/datalayer-jupyter)
- [Playbooks](https://playbooks.com/mcp/datalayer-jupyter)
- [Know That AI](https://knowthat.ai/agents/jupyter-server)

<!--
- [Smithery](https://smithery.ai/server/@datalayer/jupyter-mcp-server)
-->

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/__init__.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Tools package for Jupyter MCP Server.

Each tool is implemented as a separate class with an execute method
that can operate in either MCP_SERVER or JUPYTER_SERVER mode.
"""

from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.tools._registry import ToolRegistry, get_tool_registry, register_tool

# Import tool implementations - Notebook Management
from jupyter_mcp_server.tools.list_notebooks_tool import ListNotebooksTool
from jupyter_mcp_server.tools.restart_notebook_tool import RestartNotebookTool
from jupyter_mcp_server.tools.unuse_notebook_tool import UnuseNotebookTool
from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool

# Import tool implementations - Cell Reading
from jupyter_mcp_server.tools.read_cells_tool import ReadCellsTool
from jupyter_mcp_server.tools.list_cells_tool import ListCellsTool
from jupyter_mcp_server.tools.read_cell_tool import ReadCellTool

# Import tool implementations - Cell Writing
from jupyter_mcp_server.tools.insert_cell_tool import InsertCellTool
from jupyter_mcp_server.tools.insert_execute_code_cell_tool import InsertExecuteCodeCellTool
from jupyter_mcp_server.tools.overwrite_cell_source_tool import OverwriteCellSourceTool
from jupyter_mcp_server.tools.delete_cell_tool import DeleteCellTool

# Import tool implementations - Cell Execution
from jupyter_mcp_server.tools.execute_cell_tool import ExecuteCellTool

# Import tool implementations - Other Tools
from jupyter_mcp_server.tools.assign_kernel_to_notebook_tool import AssignKernelToNotebookTool
from jupyter_mcp_server.tools.execute_ipython_tool import ExecuteIpythonTool
from jupyter_mcp_server.tools.list_files_tool import ListFilesTool
from jupyter_mcp_server.tools.list_kernels_tool import ListKernelsTool

__all__ = [
    "BaseTool",
    "ServerMode",
    "ToolRegistry",
    "get_tool_registry",
    "register_tool",
    # Notebook Management
    "ListNotebooksTool",
    "RestartNotebookTool",
    "UnuseNotebookTool",
    "UseNotebookTool",
    # Cell Reading
    "ReadCellsTool",
    "ListCellsTool",
    "ReadCellTool",
    # Cell Writing
    "InsertCellTool",
    "InsertExecuteCodeCellTool",
    "OverwriteCellSourceTool",
    "DeleteCellTool",
    # Cell Execution
    "ExecuteCellTool",
    # Other Tools
    "AssignKernelToNotebookTool",
    "ExecuteIpythonTool",
    "ListFilesTool",
    "ListKernelsTool",
]



```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

[build-system]
requires = ["hatchling~=1.21"]
build-backend = "hatchling.build"

[project]
name = "jupyter_mcp_server"
authors = [{ name = "Datalayer", email = "[email protected]" }]
dynamic = ["version"]
readme = "README.md"
requires-python = ">=3.10"
keywords = ["Jupyter"]
classifiers = [
  "Intended Audience :: Developers",
  "Intended Audience :: System Administrators",
  "License :: OSI Approved :: BSD License",
  "Programming Language :: Python",
  "Programming Language :: Python :: 3",
]
dependencies = [
    "jupyter-server-nbmodel",
    "jupyter-kernel-client>=0.7.3",
    "jupyter-nbmodel-client>=0.14.2",
    "jupyter-server-api",
    "jupyter_server>=1.6,<3",
    "tornado>=6.1",
    "traitlets>=5.0",
    "mcp[cli]>=1.10.1",
    "pydantic",
    "uvicorn",
    "click",
    "fastapi"
]

[project.optional-dependencies]
test = [
    "ipykernel", 
    "jupyter_server>=1.6,<3", 
    "pytest>=7.0", 
    "pytest-asyncio",
    "pytest-timeout>=2.1.0",
    "jupyterlab==4.4.1",
    "jupyter-collaboration==4.0.2",
    "datalayer_pycrdt==0.12.17",
    "pillow>=10.0.0"
]
lint = ["mdformat>0.7", "mdformat-gfm>=0.3.5", "ruff"]
typing = ["mypy>=0.990"]

[project.scripts]
jupyter-mcp-server = "jupyter_mcp_server.server:server"

[project.license]
file = "LICENSE"

[project.urls]
Home = "https://github.com/datalayer/jupyter-mcp-server"

[tool.hatch.version]
path = "jupyter_mcp_server/__version__.py"

[tool.hatch.build]
include = [
  "jupyter_mcp_server/**/*.py",
  "jupyter-config/**/*.json"
]

[tool.hatch.build.targets.wheel.shared-data]
"jupyter-config/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d"
"jupyter-config/jupyter_notebook_config.d" = "etc/jupyter/jupyter_notebook_config.d"

[tool.pytest.ini_options]
filterwarnings = [
  "error",
  "ignore:There is no current event loop:DeprecationWarning",
  "module:make_current is deprecated:DeprecationWarning",
  "module:clear_current is deprecated:DeprecationWarning",
  "module:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning",
]

[tool.mypy]
check_untyped_defs = true
disallow_incomplete_defs = true
no_implicit_optional = true
pretty = true
show_error_context = true
show_error_codes = true
strict_equality = true
warn_unused_configs = true
warn_unused_ignores = true
warn_redundant_casts = true

[tool.ruff]
target-version = "py310"
line-length = 100

[tool.ruff.lint]
select = [
  "A",
  "B",
  "C",
  "E",
  "F",
  "FBT",
  "I",
  "N",
  "Q",
  "RUF",
  "S",
  "T",
  "UP",
  "W",
  "YTT",
]
ignore = [
  # FBT001 Boolean positional arg in function definition
  "FBT001",
  "FBT002",
  "FBT003",
]

[tool.ruff.lint.per-file-ignores]
# S101 Use of `assert` detected
"jupyter_mcp_server/tests/*" = ["S101"]

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/streamable-http/standalone/index.mdx:
--------------------------------------------------------------------------------

```markdown
# As a Standalone Server

## 1. Start JupyterLab

### Environment setup

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

```bash
pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel
pip uninstall -y pycrdt datalayer_pycrdt
pip install datalayer_pycrdt==0.12.17
```

### JupyterLab start

Then, start JupyterLab with the following command.

```bash
jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0
```

You can also run `make jupyterlab` if you cloned the repository.

:::note

The `--ip` is set to `0.0.0.0` to allow the MCP Server running in a Docker container to access your local JupyterLab.

:::

:::info
For JupyterHub:
- Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment.
- Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub.
:::

## 2. Setup Jupyter MCP Server

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.

### Run the Jupyter MCP Server

#### Run with Python

Clone the repository and use `pip install -e .` or just install the `jupyter-mcp-server package` from PyPI with `pip install jupyter-mcp-server`.

Then, you can start Jupyter MCP Server with the following commands.

```bash
# make start
jupyter-mcp-server start \
	  --transport streamable-http \
	  --document-url http://localhost:8888 \
	  --document-id notebook.ipynb \
	  --document-token MY_TOKEN \
	  --runtime-url http://localhost:8888 \
	  --start-new-runtime true \
	  --runtime-token MY_TOKEN \
	  --port 4040
```

#### Run with Docker

:::note

  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.

:::

You can also run the Jupyter MCP Server using Docker. Use the following command to start the server on **MacOS**.

```bash
docker run \
  -e DOCUMENT_URL="http://localhost:8888" \
  -p 4040:4040 \
  datalayer/jupyter-mcp-server:latest \
  --transport streamable-http
```

Use the following command to start the server on **Linux**.

```bash
docker run \
  --network=host \
  -e DOCUMENT_URL="http://localhost:8888" \
  -p 4040:4040 \
  datalayer/jupyter-mcp-server:latest \
  --transport streamable-http
```

<!--

## Run with Smithery

To install Jupyter MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@datalayer/jupyter-mcp-server):

```bash
npx -y @smithery/cli install @datalayer/jupyter-mcp-server --client claude
```

-->

### Configure your MCP Client

Use the following configuration for you MCP client to connect to a running Jupyter MCP Server.

```json
{
  "mcpServers": {
    "jupyter": {
        "command": "npx",
        "args": ["mcp-remote", "http://127.0.0.1:4040/mcp"]
    }
  }
}
```

```

--------------------------------------------------------------------------------
/docs/docs/clients/vscode/index.mdx:
--------------------------------------------------------------------------------

```markdown
# VS Code

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

## Install VS Code

Download VS Code from the [official site](https://code.visualstudio.com/Download) and install it.

## Install GitHub Copilot Extension

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.

## Configure Jupyter MCP Server

There are two ways to configure the Jupyter MCP Server in VS Code: user settings or workspace settings. Once configured, restart VS Code.

:::note

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.

:::

### As User Settings in `settings.json`

Open your `settings.json`:

- Press `Ctrl+Shift+P` (or `⌘⇧P` on macOS) to open the **Command Palette**
- Type and select: **Preferences: Open Settings (JSON)**
  [Or click this command link inside VS Code](command:workbench.action.openSettingsJson)

Then add the following configuration:

```jsonc
{
  "mcp": {
    "servers": {
    "DatalayerJupyter": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "DOCUMENT_URL",
        "-e",
        "DOCUMENT_TOKEN",
        "-e",
        "DOCUMENT_ID",
        "-e",
        "RUNTIME_URL",
        "-e",
        "RUNTIME_TOKEN",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://host.docker.internal:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://host.docker.internal:8888",
        "RUNTIME_TOKEN": "MY_TOKEN"
      }
    }
  }
}
```

Update with the actual configuration details from the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server).

###  As Workspace Settings in `.vscode/mcp.json`

Open or create a `.vscode/mcp.json` file in your workspace root directory. Then add the following example configuration:

```jsonc
{
  "servers": {
    "DatalayerJupyter": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "DOCUMENT_URL",
        "-e",
        "DOCUMENT_TOKEN",
        "-e",
        "DOCUMENT_ID",
        "-e",
        "RUNTIME_URL",
        "-e",
        "RUNTIME_TOKEN",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://host.docker.internal:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://host.docker.internal:8888",
        "RUNTIME_TOKEN": "MY_TOKEN"
      }
    }
  }
}
```

Update with the actual configuration details from the [Jupyter MCP Server configuration](/deployment/jupyter/stdio#2-setup-jupyter-mcp-server).

This enables workspace-specific configuration and sharing.

## Use MCP Tools in Agent Mode

1. Launch Copilot Chat (`Ctrl+Alt+I` / `⌃⌘I`)
2. Switch to **Agent** mode from the dropdown
3. Click the **Tools** ⚙️ icon to manage Jupyter MCP Server tools
4. Use `#toolName` to invoke tools manually, or let Copilot invoke them automatically
5. Confirm tool actions when prompted (once or always)

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/restart_notebook_tool.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Restart notebook tool implementation."""

import logging
from typing import Any, Optional
from jupyter_server_api import JupyterServerClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager

logger = logging.getLogger(__name__)


class RestartNotebookTool(BaseTool):
    """Tool to restart the kernel for a specific notebook.
    
    Supports both MCP_SERVER and JUPYTER_SERVER modes.
    """
    
    @property
    def name(self) -> str:
        return "restart_notebook"
    
    @property
    def description(self) -> str:
        return """Restart the kernel for a specific notebook.
    
Args:
    notebook_name: Notebook identifier to restart
    
Returns:
    str: Success message"""
    
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[Any] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        notebook_manager: Optional[NotebookManager] = None,
        # Tool-specific parameters
        notebook_name: str = None,
        **kwargs
    ) -> str:
        """Execute the restart_notebook tool.
        
        Args:
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            kernel_manager: Kernel manager for JUPYTER_SERVER mode
            notebook_manager: Notebook manager instance
            notebook_name: Notebook identifier to restart
            **kwargs: Additional parameters
            
        Returns:
            Success message
        """
        if notebook_name not in notebook_manager:
            return f"Notebook '{notebook_name}' is not connected."
        
        if mode == ServerMode.JUPYTER_SERVER:
            # JUPYTER_SERVER mode: Use kernel_manager to restart the kernel
            if kernel_manager is None:
                return f"Failed to restart notebook '{notebook_name}': kernel_manager is required in JUPYTER_SERVER mode."
            
            # Get kernel ID from notebook_manager
            kernel_id = notebook_manager.get_kernel_id(notebook_name)
            if not kernel_id:
                return f"Failed to restart notebook '{notebook_name}': kernel ID not found."
            
            try:
                logger.info(f"Restarting kernel {kernel_id} for notebook '{notebook_name}' in JUPYTER_SERVER mode")
                await kernel_manager.restart_kernel(kernel_id)
                return f"Notebook '{notebook_name}' kernel restarted successfully. Memory state and imported packages have been cleared."
            except Exception as e:
                logger.error(f"Failed to restart kernel {kernel_id}: {e}")
                return f"Failed to restart notebook '{notebook_name}': {e}"
        
        elif mode == ServerMode.MCP_SERVER:
            # MCP_SERVER mode: Use notebook_manager's restart_notebook method
            success = notebook_manager.restart_notebook(notebook_name)
            
            if success:
                return f"Notebook '{notebook_name}' kernel restarted successfully. Memory state and imported packages have been cleared."
            else:
                return f"Failed to restart notebook '{notebook_name}'. The kernel may not support restart operation."
        else:
            return f"Invalid mode: {mode}"

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/_registry.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Tool registry and integration module."""

from typing import Dict, Any, Optional
from jupyter_server_api import JupyterServerClient
from jupyter_kernel_client import KernelClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager
from jupyter_mcp_server.config import get_config


class ToolRegistry:
    """Registry for managing and executing MCP tools."""
    
    def __init__(self):
        self._tools: Dict[str, BaseTool] = {}
        self._notebook_manager: Optional[NotebookManager] = None
    
    def register(self, tool: BaseTool):
        """Register a tool instance."""
        self._tools[tool.name] = tool
    
    def set_notebook_manager(self, notebook_manager: NotebookManager):
        """Set the notebook manager instance."""
        self._notebook_manager = notebook_manager
    
    def get_tool(self, name: str) -> Optional[BaseTool]:
        """Get a tool by name."""
        return self._tools.get(name)
    
    def list_tools(self):
        """List all registered tools."""
        return list(self._tools.values())
    
    async def execute_tool(
        self,
        tool_name: str,
        mode: ServerMode,
        **kwargs
    ) -> Any:
        """Execute a tool by name.
        
        Args:
            tool_name: Name of the tool to execute
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            **kwargs: Tool-specific parameters
            
        Returns:
            Tool execution result
        """
        tool = self.get_tool(tool_name)
        if not tool:
            raise ValueError(f"Tool '{tool_name}' not found")
        
        # Prepare common parameters based on mode
        config = get_config()
        
        if mode == ServerMode.MCP_SERVER:
            # Create HTTP clients for remote access
            server_client = JupyterServerClient(
                base_url=config.runtime_url,
                token=config.runtime_token
            )
            kernel_client = KernelClient(
                server_url=config.runtime_url,
                token=config.runtime_token,
                kernel_id=config.runtime_id
            )
            
            return await tool.execute(
                mode=mode,
                server_client=server_client,
                kernel_client=kernel_client,
                contents_manager=None,
                kernel_manager=None,
                kernel_spec_manager=None,
                notebook_manager=self._notebook_manager,
                server_url=config.runtime_url,
                token=config.runtime_token,
                **kwargs
            )
        
        else:  # JUPYTER_SERVER mode
            # Get managers from ServerContext
            from jupyter_mcp_server.jupyter_extension.context import get_server_context
            context = get_server_context()
            
            contents_manager = context.get_contents_manager()
            kernel_manager = context.get_kernel_manager()
            kernel_spec_manager = context.get_kernel_spec_manager()
            
            return await tool.execute(
                mode=mode,
                server_client=None,
                kernel_client=None,
                contents_manager=contents_manager,
                kernel_manager=kernel_manager,
                kernel_spec_manager=kernel_spec_manager,
                notebook_manager=self._notebook_manager,
                server_url=config.runtime_url,
                token=config.runtime_token,
                **kwargs
            )


# Global registry instance
_registry = ToolRegistry()


def get_tool_registry() -> ToolRegistry:
    """Get the global tool registry instance."""
    return _registry


def register_tool(tool: BaseTool):
    """Register a tool with the global registry."""
    _registry.register(tool)

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/unuse_notebook_tool.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Unuse notebook tool implementation."""

import logging
from typing import Any, Optional
from jupyter_server_api import JupyterServerClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager

logger = logging.getLogger(__name__)


class UnuseNotebookTool(BaseTool):
    """Tool to unuse from a notebook and release its resources.
    
    Supports both MCP_SERVER and JUPYTER_SERVER modes.
    """
    
    @property
    def name(self) -> str:
        return "unuse_notebook"
    
    @property
    def description(self) -> str:
        return """Unuse a specific notebook and release its resources.
    
Args:
    notebook_name: Notebook identifier to unuse
    
Returns:
    str: Success message"""
    
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[Any] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        notebook_manager: Optional[NotebookManager] = None,
        # Tool-specific parameters
        notebook_name: str = None,
        **kwargs
    ) -> str:
        """Execute the unuse_notebook tool.
        
        Args:
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            kernel_manager: Kernel manager for JUPYTER_SERVER mode (optional kernel shutdown)
            notebook_manager: Notebook manager instance
            notebook_name: Notebook identifier to disconnect
            **kwargs: Additional parameters
            
        Returns:
            Success message
        """
        if notebook_name not in notebook_manager:
            return f"Notebook '{notebook_name}' is not connected."
        
        # Get info about which notebook was current
        current_notebook = notebook_manager.get_current_notebook()
        was_current = current_notebook == notebook_name
        
        if mode == ServerMode.JUPYTER_SERVER:
            # JUPYTER_SERVER mode: Optionally shutdown kernel before removing
            # Note: In JUPYTER_SERVER mode, kernel lifecycle is managed by kernel_manager
            # We only remove the reference in notebook_manager, the actual kernel
            # continues to run unless explicitly shutdown
            
            kernel_id = notebook_manager.get_kernel_id(notebook_name)
            if kernel_id and kernel_manager:
                try:
                    logger.info(f"Notebook '{notebook_name}' is being unused in JUPYTER_SERVER mode. Kernel {kernel_id} remains running.")
                    # Optional: Uncomment to shutdown kernel when unused
                    # await kernel_manager.shutdown_kernel(kernel_id)
                    # logger.info(f"Kernel {kernel_id} shutdown successfully")
                except Exception as e:
                    logger.warning(f"Note: Could not access kernel {kernel_id}: {e}")
            
            success = notebook_manager.remove_notebook(notebook_name)
            
        elif mode == ServerMode.MCP_SERVER:
            # MCP_SERVER mode: Use notebook_manager's remove_notebook method
            # which handles KernelClient cleanup automatically
            success = notebook_manager.remove_notebook(notebook_name)
        else:
            return f"Invalid mode: {mode}"
        
        if success:
            message = f"Notebook '{notebook_name}' unused successfully."
            
            if was_current:
                new_current = notebook_manager.get_current_notebook()
                if new_current:
                    message += f" Current notebook switched to '{new_current}'."
                else:
                    message += " No notebooks remaining."
            
            return message
        else:
            return f"Notebook '{notebook_name}' was not found."

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/config.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

from typing import Optional
from pydantic import BaseModel, Field


class JupyterMCPConfig(BaseModel):
    """Singleton configuration object for Jupyter MCP Server."""
    
    # Transport configuration
    transport: str = Field(default="stdio", description="The transport to use for the MCP server")
    
    # Provider configuration  
    provider: str = Field(default="jupyter", description="The provider to use for the document and runtime")
    
    # Runtime configuration
    runtime_url: str = Field(default="http://localhost:8888", description="The runtime URL to use, or 'local' for direct serverapp access")
    start_new_runtime: bool = Field(default=False, description="Start a new runtime or use an existing one")
    runtime_id: Optional[str] = Field(default=None, description="The kernel ID to use")
    runtime_token: Optional[str] = Field(default=None, description="The runtime token to use for authentication")
    
    # Document configuration
    document_url: str = Field(default="http://localhost:8888", description="The document URL to use, or 'local' for direct serverapp access")
    document_id: Optional[str] = Field(default=None, description="The document id to use. Optional - if omitted, can list and select notebooks interactively")
    document_token: Optional[str] = Field(default=None, description="The document token to use for authentication")
    
    # Server configuration
    port: int = Field(default=4040, description="The port to use for the Streamable HTTP transport")
    
    class Config:
        """Pydantic configuration."""
        validate_assignment = True
        arbitrary_types_allowed = True
    
    def is_local_document(self) -> bool:
        """Check if document URL is set to local."""
        return self.document_url == "local"
    
    def is_local_runtime(self) -> bool:
        """Check if runtime URL is set to local."""
        return self.runtime_url == "local"


# Singleton instance
_config_instance: Optional[JupyterMCPConfig] = None


def get_config() -> JupyterMCPConfig:
    """Get the singleton configuration instance."""
    global _config_instance
    if _config_instance is None:
        _config_instance = JupyterMCPConfig()
    return _config_instance


def set_config(**kwargs) -> JupyterMCPConfig:
    """Set configuration values and return the config instance.
    
    Automatically handles string representations of None by removing them from kwargs,
    allowing defaults to be used instead. This handles cases where environment variables
    or MCP clients pass "None" as a string.
    """
    def should_skip(value):
        """Check if value is a string representation of None that should be skipped."""
        return isinstance(value, str) and value.lower() in ("none", "null", "")
    
    # Filter out string "None" values and let defaults be used instead
    # For optional fields (tokens, runtime_id, document_id), convert to actual None
    normalized_kwargs = {}
    for key, value in kwargs.items():
        if should_skip(value):
            # For optional fields, set to None; for required fields, skip (use default)
            if key in ("runtime_token", "document_token", "runtime_id", "document_id"):
                normalized_kwargs[key] = None
            # For required string fields like runtime_url, document_url, skip the key
            # to let the default value be used
            # Do nothing - skip this key
        else:
            normalized_kwargs[key] = value
    
    global _config_instance
    if _config_instance is None:
        _config_instance = JupyterMCPConfig(**normalized_kwargs)
    else:
        for key, value in normalized_kwargs.items():
            if hasattr(_config_instance, key):
                setattr(_config_instance, key, value)
    return _config_instance


def reset_config() -> JupyterMCPConfig:
    """Reset configuration to defaults."""
    global _config_instance
    _config_instance = JupyterMCPConfig()
    return _config_instance

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/enroll.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Auto-enrollment functionality for Jupyter MCP Server."""

import logging
from typing import Any

from jupyter_mcp_server.notebook_manager import NotebookManager
from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool

logger = logging.getLogger(__name__)


async def auto_enroll_document(
    config: Any,
    notebook_manager: NotebookManager,
    use_notebook_tool: UseNotebookTool,
    server_context: Any,
) -> None:
    """Automatically enroll the configured document_id as a managed notebook.
    
    Handles kernel creation/connection based on configuration:
    - If runtime_id is provided: Connect to that specific kernel
    - If start_new_runtime is True: Create a new kernel
    - If both are False/None: Enroll notebook WITHOUT kernel (notebook-only mode)
    
    Args:
        config: JupyterMCPConfig instance with configuration parameters
        notebook_manager: NotebookManager instance for managing notebooks
        use_notebook_tool: UseNotebookTool instance for enrolling notebooks
        server_context: ServerContext instance with server state
    """
    # Check if document_id is configured and not already managed
    if not config.document_id:
        logger.debug("No document_id configured, skipping auto-enrollment")
        return
        
    if "default" in notebook_manager:
        logger.debug("Default notebook already enrolled, skipping auto-enrollment")
        return
    
    # Check if we should skip kernel creation entirely
    if not config.runtime_id and not config.start_new_runtime:
        # Enroll notebook without kernel - just register the notebook path
        try:
            logger.info(f"Auto-enrolling document '{config.document_id}' without kernel (notebook-only mode)")
            # Add notebook to manager without kernel
            notebook_manager.add_notebook(
                "default",
                None,  # No kernel
                server_url=config.document_url,
                token=config.document_token,
                path=config.document_id
            )
            notebook_manager.set_current_notebook("default")
            logger.info(f"Auto-enrollment result: Successfully enrolled notebook 'default' at path '{config.document_id}' without kernel.")
            return
        except Exception as e:
            logger.warning(f"Failed to auto-enroll document without kernel: {e}")
            return
    
    # Otherwise, enroll with kernel
    try:
        # Determine kernel_id based on configuration
        kernel_id_to_use = None
        if config.runtime_id:
            # User explicitly provided a kernel ID to connect to
            kernel_id_to_use = config.runtime_id
            logger.info(f"Auto-enrolling document '{config.document_id}' with existing kernel '{kernel_id_to_use}'")
        elif config.start_new_runtime:
            # User wants a new kernel created
            kernel_id_to_use = None  # Will trigger new kernel creation in use_notebook_tool
            logger.info(f"Auto-enrolling document '{config.document_id}' with new kernel")
        
        # Use the use_notebook_tool to properly enroll the notebook with kernel
        result = await use_notebook_tool.execute(
            mode=server_context.mode,
            server_client=server_context.server_client,
            notebook_name="default",
            notebook_path=config.document_id,
            use_mode="connect",
            kernel_id=kernel_id_to_use,
            contents_manager=server_context.contents_manager,
            kernel_manager=server_context.kernel_manager,
            session_manager=server_context.session_manager,
            notebook_manager=notebook_manager,
            runtime_url=config.runtime_url if config.runtime_url != "local" else None,
            runtime_token=config.runtime_token,
        )
        logger.info(f"Auto-enrollment result: {result}")
    except Exception as e:
        logger.warning(f"Failed to auto-enroll document: {e}. You can manually use it with use_notebook tool.")

```

--------------------------------------------------------------------------------
/examples/integration_example.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Example integration of the new tool architecture into server.py.

This demonstrates how to:
1. Register tool instances with the registry
2. Wrap them with @mcp.tool() decorators
3. Determine the server mode and call tool.execute()
"""

from typing import Optional
from jupyter_mcp_server.tools._base import ServerMode
from jupyter_mcp_server.tools._registry import get_tool_registry, register_tool
from jupyter_mcp_server.tools.list_notebooks_tool import ListNotebooksTool
from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool


# Initialize and register tools
def initialize_tools():
    """Register all tool instances."""
    register_tool(ListNotebooksTool())
    register_tool(UseNotebookTool())
    # ... register other tools as they are created
    

# Example of how to wrap a tool with @mcp.tool() decorator
def register_mcp_tools(mcp, notebook_manager):
    """Register tools with FastMCP server.
    
    Args:
        mcp: FastMCP instance
        notebook_manager: NotebookManager instance
    """
    registry = get_tool_registry()
    registry.set_notebook_manager(notebook_manager)
    
    @mcp.tool()
    async def list_notebook() -> str:
        """List all notebooks in the Jupyter server (including subdirectories) and show which ones are managed.
        
        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.
        
        Returns:
            str: TSV formatted table with notebook information including management status
        """
        # Determine server mode
        mode = _get_server_mode()
        
        # Execute the tool
        return await registry.execute_tool(
            "list_notebooks",
            mode=mode
        )
    
    @mcp.tool()
    async def use_notebook(
        notebook_name: str,
        notebook_path: str,
        mode: str = "connect",  # Renamed parameter to avoid conflict
        kernel_id: Optional[str] = None,
    ) -> str:
        """Connect to a notebook file or create a new one.
        
        Args:
            notebook_name: Unique identifier for the notebook
            notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb")
            mode: "connect" to connect to existing, "create" to create new
            kernel_id: Specific kernel ID to use (optional, will create new if not provided)
            
        Returns:
            str: Success message with notebook information
        """
        # Determine server mode
        server_mode = _get_server_mode()
        
        # Execute the tool
        return await registry.execute_tool(
            "use_notebook",
            mode=server_mode,
            notebook_name=notebook_name,
            notebook_path=notebook_path,
            operation_mode=mode,  # Map to tool's parameter name
            kernel_id=kernel_id
        )
    
    # ... register other tools similarly


def _get_server_mode() -> ServerMode:
    """Determine which server mode we're running in.
    
    Returns:
        ServerMode.JUPYTER_SERVER if running as Jupyter extension with local access
        ServerMode.MCP_SERVER if running standalone with HTTP clients
    """
    try:
        from jupyter_mcp_server.jupyter_extension.context import get_server_context
        context = get_server_context()
        
        # Check if we're in Jupyter server mode with local access
        if (context.context_type == "JUPYTER_SERVER" and 
            context.is_local_document() and 
            context.get_contents_manager() is not None):
            return ServerMode.JUPYTER_SERVER
    except ImportError:
        # Context module not available, must be MCP_SERVER mode
        pass
    except Exception:
        # Any error checking context, default to MCP_SERVER
        pass
    
    return ServerMode.MCP_SERVER


# Example usage in server.py:
# 
# # After creating mcp and notebook_manager instances:
# initialize_tools()
# register_mcp_tools(mcp, notebook_manager)

```

--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------

```javascript
/*
 * Copyright (c) 2023-2024 Datalayer, Inc.
 *
 * BSD 3-Clause License
 */

/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = {
  title: '🪐 ✨ Jupyter MCP Server documentation',
  tagline: 'Tansform your Notebooks into an interactive, AI-powered workspace that adapts to your needs!',
  url: 'https://datalayer.ai',
  baseUrl: '/',
  onBrokenLinks: 'throw',
  onBrokenMarkdownLinks: 'warn',
  favicon: 'img/favicon.ico',
  organizationName: 'datalayer', // Usually your GitHub org/user name.
  projectName: 'jupyter-mcp-server', // Usually your repo name.
  markdown: {
    mermaid: true,
  },
  plugins: [
    '@docusaurus/theme-live-codeblock',
    'docusaurus-lunr-search',
  ],
  themes: [
    '@docusaurus/theme-mermaid',
  ],
  themeConfig: {
    colorMode: {
      defaultMode: 'light',
      disableSwitch: true,
    },
    navbar: {
      title: 'Jupyter MCP Server Docs',
      logo: {
        alt: 'Datalayer Logo',
        src: 'img/datalayer/logo.svg',
      },
      items: [
        {
          href: 'https://discord.gg/YQFwvmSSuR',
          position: 'right',
          className: 'header-discord-link',
          'aria-label': 'Discord',
        },
        {
          href: 'https://github.com/datalayer/jupyter-mcp-server',
          position: 'right',
          className: 'header-github-link',
          'aria-label': 'GitHub',
        },
        {
          href: 'https://bsky.app/profile/datalayer.ai',
          position: 'right',
          className: 'header-bluesky-link',
          'aria-label': 'Bluesky',
        },
        {
          href: 'https://x.com/DatalayerIO',
          position: 'right',
          className: 'header-x-link',
          'aria-label': 'X',
        },
        {
          href: 'https://www.linkedin.com/company/datalayer',
          position: 'right',
          className: 'header-linkedin-link',
          'aria-label': 'LinkedIn',
        },
        {
          href: 'https://tiktok.com/@datalayerio',
          position: 'right',
          className: 'header-tiktok-link',
          'aria-label': 'TikTok',
        },
        {
          href: 'https://www.youtube.com/@datalayer',
          position: 'right',
          className: 'header-youtube-link',
          'aria-label': 'YouTube',
        },
        {
          href: 'https://datalayer.io',
          position: 'right',
          className: 'header-datalayer-io-link',
          'aria-label': 'Datalayer',
        },
      ],
    },
    footer: {
      style: 'dark',
      links: [
        {
          title: 'Docs',
          items: [
            {
              label: 'Jupyter MCP Server',
              to: '/',
            },
          ],
        },
        {
          title: 'Community',
          items: [
            {
              label: 'GitHub',
              href: 'https://github.com/datalayer',
            },
            {
              label: 'Bluesky',
              href: 'https://assets.datalayer.tech/logos-social-grey/youtube.svg',
            },
            {
              label: 'LinkedIn',
              href: 'https://www.linkedin.com/company/datalayer',
            },
          ],
        },
        {
          title: 'More',
          items: [
            {
              label: 'Datalayer',
              href: 'https://datalayer.ai',
            },
            {
              label: 'Datalayer Docs',
              href: 'https://docs.datalayer.ai',
            },
            {
              label: 'Datalayer Blog',
              href: 'https://datalayer.blog',
            },
            {
              label: 'Datalayer Guide',
              href: 'https://datalayer.guide',
            },
          ],
        },
      ],
      copyright: `Copyright © ${new Date().getFullYear()} Datalayer, Inc.`,
    },
  },
  presets: [
    [
      '@docusaurus/preset-classic',
      {
        docs: {
          routeBasePath: '/',
          docItemComponent: '@theme/CustomDocItem',
          sidebarPath: require.resolve('./sidebars.js'),
          editUrl: 'https://github.com/datalayer/jupyter-mcp-server/edit/main/',
        },
        theme: {
          customCss: require.resolve('./src/css/custom.css'),
        },
        gtag: {
          trackingID: 'G-EYRGHH1GN6',
          anonymizeIP: false,
        },
      },
    ],
  ],
};

```

--------------------------------------------------------------------------------
/docs/docs/deployment/jupyter/stdio/index.mdx:
--------------------------------------------------------------------------------

```markdown
# STDIO Transport

## 1. Start JupyterLab

### Environment setup

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

```bash
pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel
pip uninstall -y pycrdt datalayer_pycrdt
pip install datalayer_pycrdt==0.12.17
```

### JupyterLab start

Then, start JupyterLab with the following command.

```bash
jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0
```

You can also run `make jupyterlab` if you cloned the repository.

:::note

The `--ip` is set to `0.0.0.0` to allow the MCP Server running in a Docker container to access your local JupyterLab.

:::

:::info
For JupyterHub:
- Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment.
- Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub.
:::

## 2. Setup Jupyter MCP Server

You can choose between two deployment methods: **uvx** (lightweight and faster, recommended for first try) or **Docker** (recommended for production).

:::important

- Ensure the `port` of the `DOCUMENT_URL` and `RUNTIME_URL` match those used in the `jupyter lab` command.

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

- 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.
  - **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.
  - **Flexible:** Even if you set `DOCUMENT_ID`, the MCP client can still browse, list, switch to, or even create new notebooks at any time.

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

:::

### Using UVX (Quick Start)

Make sure you have `uv` installed. You can install it via pip:

```bash
pip install uv
uv --version
# should be 0.6.14 or higher
```
See more details on [uv installation](https://docs.astral.sh/uv/getting-started/installation/).

Use the following configuration file to set up the Jupyter MCP Server for your preferred MCP client.

```json
{
  "mcpServers": {
    "jupyter": {
      "command": "uvx",
      "args": ["jupyter-mcp-server@latest"],
      "env": {
        "DOCUMENT_URL": "http://localhost:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://localhost:8888",
        "RUNTIME_TOKEN": "MY_TOKEN",
        "ALLOW_IMG_OUTPUT": "true"
      }
    }
  }
}
```

### Using Docker (Production)

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.


#### For MacOS and Windows

```json
{
  "mcpServers": {
    "jupyter": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "DOCUMENT_URL",
        "-e", "DOCUMENT_TOKEN",
        "-e", "DOCUMENT_ID",
        "-e", "RUNTIME_URL",
        "-e", "RUNTIME_TOKEN",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://host.docker.internal:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://host.docker.internal:8888",
        "RUNTIME_TOKEN": "MY_TOKEN"
      }
    }
  }
}
```

#### For Linux

```json
{
  "mcpServers": {
    "jupyter": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "DOCUMENT_URL",
        "-e", "DOCUMENT_TOKEN",
        "-e", "DOCUMENT_ID",
        "-e", "RUNTIME_URL",
        "-e", "RUNTIME_TOKEN",
        "--network=host",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://localhost:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://localhost:8888",
        "RUNTIME_TOKEN": "MY_TOKEN"
      }
    }
  }
}
```

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/read_cells_tool.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Read all cells tool implementation."""

from typing import Any, Optional, List, Dict, Union
from jupyter_server_api import JupyterServerClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager
from jupyter_mcp_server.models import CellInfo
from jupyter_mcp_server.config import get_config
from jupyter_mcp_server.utils import get_current_notebook_context
from mcp.types import ImageContent


class ReadCellsTool(BaseTool):
    """Tool to read cells from a Jupyter notebook."""
    
    @property
    def name(self) -> str:
        return "read_cells"
    
    @property
    def description(self) -> str:
        return """Read cells from the Jupyter notebook.
    
Returns:
    list[dict]: List of cell information including index, type, source,
                and outputs (for code cells)"""
    
    async def _read_cells_local(self, contents_manager: Any, path: str) -> List[Dict[str, Any]]:
        """Read cells using local contents_manager (JUPYTER_SERVER mode)."""
        # Read the notebook file directly
        model = await contents_manager.get(path, content=True, type='notebook')
        
        if 'content' not in model:
            raise ValueError(f"Could not read notebook content from {path}")
        
        notebook_content = model['content']
        cells = notebook_content.get('cells', [])
        
        # Convert cells to the expected format using CellInfo for consistency
        result = []
        for idx, cell in enumerate(cells):
            # Use CellInfo.from_cell to ensure consistent structure and output processing
            cell_info = CellInfo.from_cell(cell_index=idx, cell=cell)
            result.append(cell_info.model_dump(exclude_none=True))
        
        return result
    
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[Any] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        notebook_manager: Optional[NotebookManager] = None,
        **kwargs
    ) -> List[Dict[str, Union[str, int, List[Union[str, ImageContent]]]]]:
        """Execute the read_cells tool.
        
        Args:
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            contents_manager: Direct API access for JUPYTER_SERVER mode
            notebook_manager: Notebook manager instance for MCP_SERVER mode
            **kwargs: Additional parameters
            
        Returns:
            List of cell information dictionaries
        """
        if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
            # Local mode: read notebook directly from file system
            from jupyter_mcp_server.jupyter_extension.context import get_server_context
            from pathlib import Path
            
            context = get_server_context()
            serverapp = context.serverapp
            
            notebook_path, _ = get_current_notebook_context(notebook_manager)
            
            # contents_manager expects path relative to serverapp.root_dir
            # If we have an absolute path, convert it to relative
            if serverapp and Path(notebook_path).is_absolute():
                root_dir = Path(serverapp.root_dir)
                abs_path = Path(notebook_path)
                try:
                    notebook_path = str(abs_path.relative_to(root_dir))
                except ValueError:
                    # Path is not under root_dir, use as-is
                    pass
            
            return await self._read_cells_local(contents_manager, notebook_path)
        elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
            # Remote mode: use WebSocket connection to Y.js document
            async with notebook_manager.get_current_connection() as notebook:
                cells = []
                total_cells = len(notebook)

                for i in range(total_cells):
                    cells.append(CellInfo.from_cell(i, notebook[i]).model_dump(exclude_none=True))
                
                return cells
        else:
            raise ValueError(f"Invalid mode or missing required clients: mode={mode}")

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/read_cell_tool.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""Read cell tool implementation."""

from typing import Any, Optional, Dict, Union, List
from jupyter_server_api import JupyterServerClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager
from jupyter_mcp_server.models import CellInfo
from jupyter_mcp_server.config import get_config
from mcp.types import ImageContent


class ReadCellTool(BaseTool):
    """Tool to read a specific cell from a notebook."""
    
    @property
    def name(self) -> str:
        return "read_cell"
    
    @property
    def description(self) -> str:
        return """Read a specific cell from the Jupyter notebook.
    
Args:
    cell_index: Index of the cell to read (0-based)
    
Returns:
    dict: Cell information including index, type, source, and outputs (for code cells)"""
    
    async def _read_cell_local(self, contents_manager: Any, path: str, cell_index: int) -> Dict[str, Any]:
        """Read a specific cell using local contents_manager (JUPYTER_SERVER mode)."""
        # Read the notebook file directly
        model = await contents_manager.get(path, content=True, type='notebook')
        
        if 'content' not in model:
            raise ValueError(f"Could not read notebook content from {path}")
        
        notebook_content = model['content']
        cells = notebook_content.get('cells', [])
        
        if cell_index < 0 or cell_index >= len(cells):
            raise ValueError(
                f"Cell index {cell_index} is out of range. Notebook has {len(cells)} cells."
            )
        
        cell = cells[cell_index]
        
        # Use CellInfo.from_cell to normalize the structure (ensures "type" field not "cell_type")
        cell_info = CellInfo.from_cell(cell_index=cell_index, cell=cell)
        
        return cell_info.model_dump(exclude_none=True)
    
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[Any] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        notebook_manager: Optional[NotebookManager] = None,
        # Tool-specific parameters
        cell_index: int = None,
        **kwargs
    ) -> Dict[str, Union[str, int, List[Union[str, ImageContent]]]]:
        """Execute the read_cell tool.
        
        Args:
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            contents_manager: Direct API access for JUPYTER_SERVER mode
            notebook_manager: Notebook manager instance
            cell_index: Index of the cell to read (0-based)
            **kwargs: Additional parameters
            
        Returns:
            Cell information dictionary
        """
        if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
            # Use local contents_manager to read the notebook
            from jupyter_mcp_server.jupyter_extension.context import get_server_context
            from pathlib import Path
            
            context = get_server_context()
            serverapp = context.serverapp
            
            notebook_path = None
            if notebook_manager:
                notebook_path = notebook_manager.get_current_notebook_path()
            if not notebook_path:
                config = get_config()
                notebook_path = config.document_id
            
            # contents_manager expects path relative to serverapp.root_dir
            # If we have an absolute path, convert it to relative
            if serverapp and Path(notebook_path).is_absolute():
                root_dir = Path(serverapp.root_dir)
                abs_path = Path(notebook_path)
                try:
                    notebook_path = str(abs_path.relative_to(root_dir))
                except ValueError:
                    # Path is not under root_dir, use as-is
                    pass
            
            return await self._read_cell_local(contents_manager, notebook_path, cell_index)
        elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
            # Remote mode: use WebSocket connection to Y.js document
            async with notebook_manager.get_current_connection() as notebook:
                if cell_index < 0 or cell_index >= len(notebook):
                    raise ValueError(f"Cell index {cell_index} out of range")

                cell = notebook[cell_index]
                return CellInfo.from_cell(cell_index=cell_index, cell=cell).model_dump(exclude_none=True)
        else:
            raise ValueError(f"Invalid mode or missing required clients: mode={mode}")

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/list_files_tool.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""List all files and directories tool."""

from typing import Any, Optional, List, Dict
from jupyter_server_api import JupyterServerClient

from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.config import get_config
from jupyter_mcp_server.utils import format_TSV


class ListFilesTool(BaseTool):
    """List files and directories in the Jupyter server's file system.
    
    This tool recursively lists files and directories from the Jupyter server's content API,
    showing the complete file structure including notebooks, data files, scripts, and directories.
    """
    
    @property
    def name(self) -> str:
        return "list_files"
    
    @property
    def description(self) -> str:
        return "List all files and directories in the Jupyter server's file system"
    
    async def _list_files_local(
        self,
        contents_manager: Any,
        path: str = "",
        max_depth: int = 3,
        current_depth: int = 0
    ) -> List[Dict[str, Any]]:
        """List files using local contents_manager API (JUPYTER_SERVER mode)."""
        all_files = []
        
        if current_depth >= max_depth:
            return all_files
        
        try:
            # Get directory contents
            model = await contents_manager.get(path, content=True, type='directory')
            
            if 'content' not in model:
                return all_files
            
            for item in model['content']:
                item_path = item['path']
                item_type = item['type']
                
                file_info = {
                    'path': item_path,
                    'type': item_type,
                    'size': item.get('size', 0) if item_type == 'file' else 0,
                    'last_modified': item.get('last_modified', '')
                }
                all_files.append(file_info)
                
                # Recursively list subdirectories
                if item_type == 'directory':
                    subfiles = await self._list_files_local(
                        contents_manager,
                        item_path,
                        max_depth,
                        current_depth + 1
                    )
                    all_files.extend(subfiles)
                    
        except Exception:
            # Directory not accessible or doesn't exist
            pass
        
        return all_files
    
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[Any] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        notebook_manager: Optional[Any] = None,
        # Tool-specific parameters
        path: str = "",
        max_depth: int = 3,
        list_files_recursively_fn=None,
        **kwargs
    ) -> str:
        """List all files and directories.
        
        Args:
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            contents_manager: Direct API access for JUPYTER_SERVER mode
            path: The starting path to list from (empty string means root directory)
            max_depth: Maximum depth to recurse into subdirectories (default: 3)
            list_files_recursively_fn: Function to recursively list files (MCP_SERVER mode)
            **kwargs: Additional parameters
            
        Returns:
            Tab-separated table with columns: Path, Type, Size, Last_Modified
        """
        if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
            # Local mode: use contents_manager directly
            all_files = await self._list_files_local(contents_manager, path, max_depth)
        elif mode == ServerMode.MCP_SERVER:
            # Remote mode: use HTTP client
            if list_files_recursively_fn is None:
                raise ValueError("list_files_recursively_fn is required for MCP_SERVER mode")
            
            config = get_config()
            server_client = JupyterServerClient(base_url=config.runtime_url, token=config.runtime_token)
            all_files = list_files_recursively_fn(server_client, path, 0, None, max_depth)
        else:
            raise ValueError(f"Invalid mode or missing required clients: mode={mode}")
        
        if not all_files:
            return f"No files found in path '{path or 'root'}'"
        
        # Sort files by path for better readability
        all_files.sort(key=lambda x: x['path'])
        
        # Create TSV formatted output
        headers = ["Path", "Type", "Size", "Last_Modified"]
        rows = []
        for file_info in all_files:
            rows.append([file_info['path'], file_info['type'], file_info['size'], file_info['last_modified']])
        
        return format_TSV(headers, rows)

```

--------------------------------------------------------------------------------
/docs/static/img/datalayer/logo.svg:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   x="0px"
   y="0px"
   width="100"
   height="100"
   viewBox="0 0 99.999997 99.999999"
   enable-background="new 0 0 130.395 175.748"
   xml:space="preserve"
   id="svg1104"
   sodipodi:docname="logo_square.svg"
   inkscape:version="0.92.2 5c3e80d, 2017-08-06"
   inkscape:export-filename="/Users/echar4/private/marketing/datalayer/logo/corporate/png/logo_square.png"
   inkscape:export-xdpi="300"
   inkscape:export-ydpi="300"><metadata
     id="metadata1110"><rdf:RDF><cc:Work
         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
     id="defs1108" /><sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1406"
     inkscape:window-height="746"
     id="namedview1106"
     showgrid="false"
     inkscape:zoom="0.94952545"
     inkscape:cx="24.718555"
     inkscape:cy="60.203158"
     inkscape:window-x="0"
     inkscape:window-y="0"
     inkscape:window-maximized="0"
     inkscape:current-layer="svg1104" /><g
     id="g439"
     transform="matrix(0.88192626,0,0,0.88192547,4694.9029,20.001364)"><linearGradient
       y2="12.7559"
       x2="-5278.1094"
       y1="12.7559"
       x1="-5295.1172"
       gradientUnits="userSpaceOnUse"
       id="SVGID_43_"><stop
         id="stop397"
         style="stop-color:#28B899"
         offset="0" /><stop
         id="stop399"
         style="stop-color:#1B937B"
         offset="1" /></linearGradient><rect
       style="fill:url(#SVGID_43_)"
       id="rect402"
       height="14.173"
       width="17.007999"
       y="5.6690001"
       x="-5295.1172" /><linearGradient
       y2="12.7559"
       x2="-5238.4248"
       y1="12.7559"
       x1="-5278.1094"
       gradientUnits="userSpaceOnUse"
       id="SVGID_44_"><stop
         id="stop404"
         style="stop-color:#03594A"
         offset="0" /><stop
         id="stop406"
         style="stop-color:#128570"
         offset="1" /></linearGradient><rect
       style="fill:url(#SVGID_44_)"
       id="rect409"
       height="14.173"
       width="39.685001"
       y="5.6690001"
       x="-5278.1089" /><linearGradient
       y2="34.014599"
       x2="-5266.7715"
       y1="34.014599"
       x1="-5295.1172"
       gradientUnits="userSpaceOnUse"
       id="SVGID_45_"><stop
         id="stop411"
         style="stop-color:#28B899"
         offset="0" /><stop
         id="stop413"
         style="stop-color:#1B937B"
         offset="1" /></linearGradient><rect
       style="fill:url(#SVGID_45_)"
       id="rect416"
       height="14.173"
       width="28.346001"
       y="26.927999"
       x="-5295.1172" /><linearGradient
       y2="34.013699"
       x2="-5238.4248"
       y1="34.013699"
       x1="-5266.7715"
       gradientUnits="userSpaceOnUse"
       id="SVGID_46_"><stop
         id="stop418"
         style="stop-color:#03594A"
         offset="0" /><stop
         id="stop420"
         style="stop-color:#128570"
         offset="1" /></linearGradient><rect
       style="fill:url(#SVGID_46_)"
       id="rect423"
       height="14.171"
       width="28.347"
       y="26.927999"
       x="-5266.771" /><linearGradient
       y2="55.274399"
       x2="-5255.4326"
       y1="55.274399"
       x1="-5295.1172"
       gradientUnits="userSpaceOnUse"
       id="SVGID_47_"><stop
         id="stop425"
         style="stop-color:#28B899"
         offset="0" /><stop
         id="stop427"
         style="stop-color:#1B937B"
         offset="1" /></linearGradient><rect
       style="fill:url(#SVGID_47_)"
       id="rect430"
       height="14.174"
       width="39.685001"
       y="48.188"
       x="-5295.1172" /><linearGradient
       y2="55.274399"
       x2="-5238.4229"
       y1="55.274399"
       x1="-5255.4326"
       gradientUnits="userSpaceOnUse"
       id="SVGID_48_"><stop
         id="stop432"
         style="stop-color:#03594A"
         offset="0" /><stop
         id="stop434"
         style="stop-color:#128570"
         offset="1" /></linearGradient><rect
       style="fill:url(#SVGID_48_)"
       id="rect437"
       height="14.174"
       width="17.01"
       y="48.188"
       x="-5255.4331" /></g></svg>
```

--------------------------------------------------------------------------------
/tests/test_use_notebook.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Unit tests for use_notebook tool with optional notebook_path parameter.

These tests verify the notebook switching functionality when notebook_path is not provided.
"""

import pytest
import logging
from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool
from jupyter_mcp_server.tools._base import ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager


@pytest.mark.asyncio
async def test_use_notebook_switching():
    """Test that use_notebook can switch between already-connected notebooks"""
    tool = UseNotebookTool()
    notebook_manager = NotebookManager()
    
    # Simulate adding two notebooks manually
    notebook_manager.add_notebook(
        "notebook_a",
        {"id": "kernel_a"},  # Mock kernel info
        server_url="local",
        token=None,
        path="notebook_a.ipynb"
    )
    
    notebook_manager.add_notebook(
        "notebook_b",
        {"id": "kernel_b"},  # Mock kernel info
        server_url="local",
        token=None,
        path="notebook_b.ipynb"
    )
    
    # Set current to notebook_a
    notebook_manager.set_current_notebook("notebook_a")
    logging.debug(f"Current notebook: {notebook_manager.get_current_notebook()}")
    assert notebook_manager.get_current_notebook() == "notebook_a"
    
    # Test switching to notebook_b (no notebook_path provided)
    result = await tool.execute(
        mode=ServerMode.JUPYTER_SERVER,
        notebook_manager=notebook_manager,
        notebook_name="notebook_b",
        notebook_path=None  # Key: no path provided, should just switch
    )
    
    logging.debug(f"Switch result: {result}")
    assert "Successfully switched to notebook 'notebook_b'" in result
    assert notebook_manager.get_current_notebook() == "notebook_b"
    
    # Test switching back to notebook_a
    result = await tool.execute(
        mode=ServerMode.JUPYTER_SERVER,
        notebook_manager=notebook_manager,
        notebook_name="notebook_a",
        notebook_path=None
    )
    
    logging.debug(f"Switch back result: {result}")
    assert "Successfully switched to notebook 'notebook_a'" in result
    assert notebook_manager.get_current_notebook() == "notebook_a"


@pytest.mark.asyncio
async def test_use_notebook_switch_to_nonexistent():
    """Test error handling when switching to non-connected notebook"""
    tool = UseNotebookTool()
    notebook_manager = NotebookManager()
    
    # Add only one notebook
    notebook_manager.add_notebook(
        "notebook_a",
        {"id": "kernel_a"},
        server_url="local",
        token=None,
        path="notebook_a.ipynb"
    )
    
    # Try to switch to non-existent notebook
    result = await tool.execute(
        mode=ServerMode.JUPYTER_SERVER,
        notebook_manager=notebook_manager,
        notebook_name="notebook_c",
        notebook_path=None
    )
    
    logging.debug(f"Non-existent notebook result: {result}")
    assert "not connected" in result
    assert "Please provide a notebook_path" in result


@pytest.mark.asyncio
async def test_use_notebook_with_path_still_works():
    """Test that providing notebook_path still works for connecting new notebooks"""
    tool = UseNotebookTool()
    notebook_manager = NotebookManager()
    
    # This should trigger the error about missing clients (since we're not providing them)
    # but it verifies the code path is still intact
    result = await tool.execute(
        mode=ServerMode.JUPYTER_SERVER,
        notebook_manager=notebook_manager,
        notebook_name="new_notebook",
        notebook_path="new.ipynb",
        use_mode="connect"
    )
    
    # Should fail because no contents_manager provided, but validates the logic path
    assert "Invalid mode or missing required clients" in result or "already using" not in result


@pytest.mark.asyncio 
async def test_use_notebook_multiple_switches():
    """Test multiple consecutive switches between notebooks"""
    tool = UseNotebookTool()
    notebook_manager = NotebookManager()
    
    # Add three notebooks
    for i, name in enumerate(["nb1", "nb2", "nb3"]):
        notebook_manager.add_notebook(
            name,
            {"id": f"kernel_{i}"},
            server_url="local",
            token=None,
            path=f"{name}.ipynb"
        )
    
    notebook_manager.set_current_notebook("nb1")
    
    # Perform multiple switches
    switches = ["nb2", "nb3", "nb1", "nb3", "nb2"]
    for target in switches:
        result = await tool.execute(
            mode=ServerMode.JUPYTER_SERVER,
            notebook_manager=notebook_manager,
            notebook_name=target,
            notebook_path=None
        )
        assert f"Successfully switched to notebook '{target}'" in result
        assert notebook_manager.get_current_notebook() == target
        logging.debug(f"Switched to {target}")


if __name__ == "__main__":
    # Allow running with: python tests/test_use_notebook.py
    pytest.main([__file__, "-v"])

```

--------------------------------------------------------------------------------
/tests/test_jupyter_extension.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Integration tests for Jupyter MCP Server in JUPYTER_SERVER mode (extension).

This test file validates the server when running as a Jupyter Server extension
with direct access to serverapp resources (contents_manager, kernel_manager).

Key differences from MCP_SERVER mode:
- Uses YDoc collaborative editing when notebooks are open
- Direct kernel_manager access for execute_ipython
- Local file operations without HTTP roundtrip

The tests connect to the extension's HTTP endpoints (not the standalone MCP server).

Launch the tests:
```
$ pytest tests/test_jupyter_extension.py -v
```
"""

import logging
from http import HTTPStatus

import pytest
import requests

from .conftest import JUPYTER_TOKEN


###############################################################################
# Unit Tests - Extension Components
###############################################################################

def test_import():
    """Test that all extension imports work."""
    from jupyter_mcp_server.jupyter_extension import extension
    from jupyter_mcp_server.jupyter_extension import handlers
    from jupyter_mcp_server.jupyter_extension import context
    logging.info("✅ All imports successful")
    assert True


def test_extension_points():
    """Test extension discovery."""
    from jupyter_mcp_server import _jupyter_server_extension_points
    points = _jupyter_server_extension_points()
    logging.info(f"Extension points: {points}")
    assert len(points) > 0
    assert "jupyter_mcp_server" in points[0]["module"]


def test_handler_creation():
    """Test that handlers can be instantiated."""
    from jupyter_mcp_server.jupyter_extension.handlers import (
        MCPSSEHandler, 
        MCPHealthHandler, 
        MCPToolsListHandler
    )
    logging.info("✅ Handlers available")
    assert MCPSSEHandler is not None
    assert MCPHealthHandler is not None
    assert MCPToolsListHandler is not None


###############################################################################
# Integration Tests - Extension Running in Jupyter
###############################################################################

def test_extension_health(jupyter_server_with_extension):
    """Test that Jupyter server with MCP extension is healthy"""
    logging.info(f"Testing Jupyter+MCP extension health ({jupyter_server_with_extension})")
    
    # Test Jupyter API is accessible
    response = requests.get(
        f"{jupyter_server_with_extension}/api/status",
        headers={"Authorization": f"token {JUPYTER_TOKEN}"},
    )
    assert response.status_code == HTTPStatus.OK
    logging.info("✅ Jupyter API is accessible")


def test_mode_comparison_documentation(jupyter_server_with_extension, jupyter_server):
    """
    Document the differences between the two server modes for future reference.
    
    This test serves as living documentation of the architecture.
    """
    logging.info("\n" + "="*80)
    logging.info("SERVER MODE COMPARISON")
    logging.info("="*80)
    
    logging.info("\nMCP_SERVER Mode (Standalone):")
    logging.info(f"  - URL: {jupyter_server}")
    logging.info("  - Started via: python -m jupyter_mcp_server --transport streamable-http")
    logging.info("  - Tools use: JupyterServerClient + KernelClient (HTTP)")
    logging.info("  - File operations: HTTP API (contents API)")
    logging.info("  - Cell operations: WebSocket messages")
    logging.info("  - Execute IPython: WebSocket to kernel")
    logging.info("  - Tests: test_mcp_server.py")
    
    logging.info("\nJUPYTER_SERVER Mode (Extension):")
    logging.info(f"  - URL: {jupyter_server_with_extension}")
    logging.info("  - Started via: jupyter lab --ServerApp.jpserver_extensions")
    logging.info("  - Tools use: Direct Python APIs (contents_manager, kernel_manager)")
    logging.info("  - File operations: Direct nbformat + YDoc collaborative")
    logging.info("  - Cell operations: YDoc when available, nbformat fallback")
    logging.info("  - Execute IPython: Direct kernel_manager.get_kernel() + ZMQ")
    logging.info("  - Tests: test_jupyter_extension.py (this file)")
    
    logging.info("\nKey Benefits of JUPYTER_SERVER Mode:")
    logging.info("  ✓ Real-time collaborative editing via YDoc")
    logging.info("  ✓ Zero-latency local operations")
    logging.info("  ✓ Direct ZMQ access to kernels")
    logging.info("  ✓ Automatic sync with JupyterLab UI")
    
    logging.info("\nKey Benefits of MCP_SERVER Mode:")
    logging.info("  ✓ Works with remote Jupyter servers")
    logging.info("  ✓ No Jupyter extension installation required")
    logging.info("  ✓ Can proxy to multiple Jupyter instances")
    logging.info("  ✓ Standard MCP protocol compatibility")
    
    logging.info("="*80 + "\n")
    
    # Both servers should be running
    assert jupyter_server is not None
    assert jupyter_server_with_extension is not None
    assert jupyter_server != jupyter_server_with_extension  # Different ports

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/context.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Server Context Management

This module provides a singleton to track the execution context (MCP_SERVER vs JUPYTER_SERVER)
and provide access to Jupyter Server resources when running as an extension.
"""

from typing import Optional, Literal, TYPE_CHECKING
import threading

if TYPE_CHECKING:
    from jupyter_server.serverapp import ServerApp


class ServerContext:
    """
    Singleton managing server execution context.
    
    This class tracks whether tools are running in standalone MCP_SERVER mode
    or embedded JUPYTER_SERVER mode, and provides access to server resources.
    """
    
    _instance: Optional['ServerContext'] = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance
    
    def __init__(self):
        if self._initialized:
            return
            
        self._initialized = True
        self._context_type: Literal["MCP_SERVER", "JUPYTER_SERVER"] = "MCP_SERVER"
        self._serverapp: Optional['ServerApp'] = None
        self._document_url: Optional[str] = None
        self._runtime_url: Optional[str] = None
    
    @property
    def context_type(self) -> Literal["MCP_SERVER", "JUPYTER_SERVER"]:
        """Get the current server context type."""
        return self._context_type
    
    @property
    def serverapp(self) -> Optional['ServerApp']:
        """Get the Jupyter ServerApp instance (only available in JUPYTER_SERVER mode)."""
        return self._serverapp
    
    @property
    def document_url(self) -> Optional[str]:
        """Get the configured document URL."""
        return self._document_url
    
    @property
    def runtime_url(self) -> Optional[str]:
        """Get the configured runtime URL."""
        return self._runtime_url
    
    def update(
        self,
        context_type: Literal["MCP_SERVER", "JUPYTER_SERVER"],
        serverapp: Optional['ServerApp'] = None,
        document_url: Optional[str] = None,
        runtime_url: Optional[str] = None
    ):
        """
        Update the server context.
        
        Args:
            context_type: The type of server context
            serverapp: Jupyter ServerApp instance (required for JUPYTER_SERVER mode)
            document_url: Document URL configuration
            runtime_url: Runtime URL configuration
        """
        with self._lock:
            self._context_type = context_type
            self._serverapp = serverapp
            self._document_url = document_url
            self._runtime_url = runtime_url
            
            if context_type == "JUPYTER_SERVER" and serverapp is None:
                raise ValueError("serverapp is required when context_type is JUPYTER_SERVER")
    
    def is_local_document(self) -> bool:
        """Check if document operations should use local serverapp."""
        return (
            self._context_type == "JUPYTER_SERVER" 
            and self._document_url == "local"
        )
    
    def is_local_runtime(self) -> bool:
        """Check if runtime operations should use local serverapp."""
        return (
            self._context_type == "JUPYTER_SERVER" 
            and self._runtime_url == "local"
        )
    
    def get_contents_manager(self):
        """
        Get the Jupyter contents manager (only available in JUPYTER_SERVER mode with local access).
        
        Returns:
            ContentsManager instance or None
        """
        if self._serverapp is not None:
            return self._serverapp.contents_manager
        return None
    
    def get_kernel_manager(self):
        """
        Get the Jupyter kernel manager (only available in JUPYTER_SERVER mode with local access).
        
        Returns:
            KernelManager instance or None
        """
        if self._serverapp is not None:
            return self._serverapp.kernel_manager
        return None
    
    def get_kernel_spec_manager(self):
        """
        Get the Jupyter kernel spec manager (only available in JUPYTER_SERVER mode with local access).
        
        Returns:
            KernelSpecManager instance or None
        """
        if self._serverapp is not None:
            return self._serverapp.kernel_spec_manager
        return None
    
    def get_session_manager(self):
        """
        Get the Jupyter session manager (only available in JUPYTER_SERVER mode with local access).
        
        Returns:
            SessionManager instance or None
        """
        if self._serverapp is not None:
            return self._serverapp.session_manager
        return None
    
    @property
    def session_manager(self):
        """
        Get the Jupyter session manager as a property (only available in JUPYTER_SERVER mode with local access).
        
        Returns:
            SessionManager instance or None
        """
        return self.get_session_manager()
    
    def reset(self):
        """Reset to default MCP_SERVER mode."""
        with self._lock:
            self._context_type = "MCP_SERVER"
            self._serverapp = None
            self._document_url = None
            self._runtime_url = None


# Global accessor
def get_server_context() -> ServerContext:
    """Get the global ServerContext singleton instance."""
    return ServerContext()

```

--------------------------------------------------------------------------------
/docs/docs/configure/index.mdx:
--------------------------------------------------------------------------------

```markdown
# Configure

## Options

Check the help for the Jupyter MCP Server to see the available configuration options.

```bash
jupyter-mcp-server start --help

Usage: jupyter-mcp-server start [OPTIONS]

  Start the Jupyter MCP server with a transport.

Options:
  --transport [stdio|streamable-http]
                                  The transport to use for the MCP server.
                                  Defaults to 'stdio'.
  --provider [jupyter|datalayer]  The provider to use for the document and
                                  runtime. Defaults to 'jupyter'.
  --runtime-url TEXT              The runtime URL to use. For the jupyter
                                  provider, this is the Jupyter server URL.
                                  For the datalayer provider, this is the
                                  Datalayer runtime URL.
  --start-new-runtime BOOLEAN     Start a new runtime or use an existing one.
  --runtime-id TEXT               The kernel ID to use. If not provided, a new
                                  kernel should be started.
  --runtime-token TEXT            The runtime token to use for authentication
                                  with the provider. If not provided, the
                                  provider should accept anonymous requests.
  --document-url TEXT             The document URL to use. For the jupyter
                                  provider, this is the Jupyter server URL.
                                  For the datalayer provider, this is the
                                  Datalayer document URL.
  --document-id TEXT              The document id to use. For the jupyter
                                  provider, this is the notebook path. For the
                                  datalayer provider, this is the notebook
                                  path.
  --document-token TEXT           The document token to use for authentication
                                  with the provider. If not provided, the
                                  provider should accept anonymous requests.
  --port INTEGER                  The port to use for the Streamable HTTP
                                  transport. Ignored for stdio transport.
  --help                          Show this message and exit
```

## Starting then Connecting to Existing Runtime

For example, you can start the MCP Server with the following command that will not create a new Runtime.

```bash
jupyter-mcp-server start \
  --transport streamable-http \
  --runtime-token MY_TOKEN \
  --document-url http://localhost:8888 \
  --runtime-url http://localhost:8888 \
  --start-new-runtime false
```

Only after you can start a local JupyterLab and open a Notebook with a Runtime.

```bash
make jupyterlab
```

Then, you can assign a document and runtime via the `/connect` endpoint by running this command.

```bash
jupyter-mcp-server connect \
  --provider datalayer \
  --document-url <url> \
  --document-id <document> \
  --document-token <token> \
  --runtime-url <url> \
  --runtime-id <runtime-id> \
  --runtime-token <token> \
  --jupyter-mcp-server-url http://localhost:4040
```

## Multimodal Output Support

The server supports multimodal output, allowing AI agents to directly receive and analyze visual content such as images and charts generated by code execution.

### Supported Output Types

- **Text Output**: Standard text output from code execution
- **Image Output**: PNG images generated by matplotlib, seaborn, plotly, and other visualization libraries
- **Error Output**: Error messages and tracebacks

### Environment Variable Configuration

Control multimodal output behavior using environment variables:

#### `ALLOW_IMG_OUTPUT`

Controls whether to return actual image content or text placeholders.

- **Default**: `true`
- **Values**: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`, `enable`, `disable`, `enabled`, `disabled`

**Example Docker Configuration:**

```json
{
  "mcpServers": {
    "jupyter": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "DOCUMENT_URL",
        "-e", "DOCUMENT_TOKEN",
        "-e", "DOCUMENT_ID",
        "-e", "RUNTIME_URL",
        "-e", "RUNTIME_TOKEN",
        "-e", "ALLOW_IMG_OUTPUT",
        "datalayer/jupyter-mcp-server:latest"
      ],
      "env": {
        "DOCUMENT_URL": "http://host.docker.internal:8888",
        "DOCUMENT_TOKEN": "MY_TOKEN",
        "DOCUMENT_ID": "notebook.ipynb",
        "RUNTIME_URL": "http://host.docker.internal:8888",
        "RUNTIME_TOKEN": "MY_TOKEN",
        "ALLOW_IMG_OUTPUT": "true"
      }
    }
  }
}
```

### Output Behavior

#### When `ALLOW_IMG_OUTPUT=true` (Default)
- Images are returned as `ImageContent` objects with actual PNG data
- AI agents can directly analyze visual content
- Supports advanced multimodal reasoning

#### When `ALLOW_IMG_OUTPUT=false`
- Images are returned as text placeholders: `"[Image Output (PNG) - Image display disabled]"`
- Maintains backward compatibility with text-only LLMs
- Reduces bandwidth and token usage

### Use Cases

**Data Visualization Analysis:**
```python
import matplotlib.pyplot as plt
import pandas as pd

df = pd.read_csv('sales_data.csv')
df.plot(kind='bar', x='month', y='revenue')
plt.title('Monthly Revenue')
plt.show()
# AI can now "see" and analyze the chart content
```

**Machine Learning Model Visualization:**
```python
import matplotlib.pyplot as plt

# Plot training curves
plt.plot(epochs, train_loss, label='Training Loss')
plt.plot(epochs, val_loss, label='Validation Loss')
plt.legend()
plt.show()
# AI can evaluate training effectiveness from the visual curves
```

```

--------------------------------------------------------------------------------
/tests/test_list_kernels.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Tests for list_kernels tool in both MCP_SERVER and JUPYTER_SERVER modes.
"""

import logging
import pytest

# Explicitly set pytest-asyncio mode for this module
pytestmark = pytest.mark.asyncio

from .test_common import MCPClient


@pytest.mark.asyncio
async def test_list_kernels(mcp_client_parametrized: MCPClient):
    """Test list_kernels functionality in both MCP_SERVER and JUPYTER_SERVER modes"""
    async with mcp_client_parametrized:
        # Call list_kernels
        kernel_list = await mcp_client_parametrized.list_kernels()
        logging.debug(f"Kernel list: {kernel_list}")
        
        # Verify result is a string
        assert isinstance(kernel_list, str), "list_kernels should return a string"
        
        # Check for either TSV header or "No kernels found" message
        has_header = "ID\tName\tDisplay_Name\tLanguage\tState\tConnections\tLast_Activity\tEnvironment" in kernel_list
        has_no_kernels_msg = "No kernels found" in kernel_list
        
        assert has_header or has_no_kernels_msg, \
            f"Kernel list should have TSV header or 'No kernels found' message, got: {kernel_list[:100]}"
        
        # Parse the output
        lines = kernel_list.strip().split('\n')
        
        # Should have at least one line (header or message)
        assert len(lines) >= 1, "Should have at least one line"
        
        # If there are running kernels (header present), verify the format
        if has_header and len(lines) > 1:
            # Check that data lines have the right number of columns
            header_cols = lines[0].split('\t')
            assert len(header_cols) == 8, f"Header should have 8 columns, got {len(header_cols)}"
            
            # Check first data line
            data_line = lines[1].split('\t')
            assert len(data_line) == 8, f"Data lines should have 8 columns, got {len(data_line)}"
            
            # Verify kernel ID is present (not empty or "unknown")
            kernel_id = data_line[0]
            assert kernel_id and kernel_id != "unknown", f"Kernel ID should not be empty or unknown, got '{kernel_id}'"
            
            # Verify kernel name is present
            kernel_name = data_line[1]
            assert kernel_name and kernel_name != "unknown", f"Kernel name should not be empty or unknown, got '{kernel_name}'"
            
            logging.info(f"Found {len(lines) - 1} running kernel(s)")
        else:
            # No kernels found - this is valid
            logging.info("No running kernels found")


@pytest.mark.asyncio
async def test_list_kernels_after_execution(mcp_client_parametrized: MCPClient):
    """Test that list_kernels shows kernel after code execution in both modes"""
    async with mcp_client_parametrized:
        # Get initial kernel list
        initial_list = await mcp_client_parametrized.list_kernels()
        logging.debug(f"Initial kernel list: {initial_list}")
        
        # Execute some code which should start a kernel
        await mcp_client_parametrized.insert_execute_code_cell(-1, "x = 1 + 1")
        
        # Now list kernels again - should have at least one
        kernel_list = await mcp_client_parametrized.list_kernels()
        logging.debug(f"Kernel list after execution: {kernel_list}")
        
        # Verify we have at least one kernel now
        lines = kernel_list.strip().split('\n')
        assert len(lines) >= 2, "Should have header and at least one kernel after code execution"
        
        # Verify kernel state is valid
        data_line = lines[1].split('\t')
        kernel_state = data_line[4]  # State is the 5th column (index 4)
        # State could be 'idle', 'busy', 'starting', etc.
        assert kernel_state != "unknown", f"Kernel state should be known, got '{kernel_state}'"
        
        # Clean up - delete the cell we created
        cell_count = await mcp_client_parametrized.get_cell_count()
        await mcp_client_parametrized.delete_cell(cell_count - 1)


@pytest.mark.asyncio
async def test_list_kernels_format(mcp_client_parametrized: MCPClient):
    """Test that list_kernels output format is consistent in both modes"""
    async with mcp_client_parametrized:
        # Ensure we have a running kernel by executing code
        initial_count = await mcp_client_parametrized.get_cell_count()
        
        await mcp_client_parametrized.insert_execute_code_cell(-1, "print('hello')")
        
        # Get kernel list
        kernel_list = await mcp_client_parametrized.list_kernels()
        
        # Parse and validate structure
        lines = kernel_list.strip().split('\n')
        assert len(lines) >= 2, "Should have header and at least one kernel"
        
        # Verify header structure
        header = lines[0]
        expected_headers = ["ID", "Name", "Display_Name", "Language", "State", "Connections", "Last_Activity", "Environment"]
        for expected_header in expected_headers:
            assert expected_header in header, f"Header should contain '{expected_header}'"
        
        # Verify data structure
        for i in range(1, len(lines)):
            data_line = lines[i].split('\t')
            assert len(data_line) == 8, f"Line {i} should have 8 columns"
            
            # ID should be a valid UUID-like string
            kernel_id = data_line[0]
            assert len(kernel_id) > 0, "Kernel ID should not be empty"
            
            # Name should not be empty
            kernel_name = data_line[1]
            assert len(kernel_name) > 0, "Kernel name should not be empty"
        
        # Clean up
        cell_count = await mcp_client_parametrized.get_cell_count()
        await mcp_client_parametrized.delete_cell(cell_count - 1)

```

--------------------------------------------------------------------------------
/docs/static/img/feature_2.svg:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
  ~ Copyright (c) 2023-2024 Datalayer, Inc.
  ~
  ~ BSD 3-Clause License
-->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   viewBox="0 0 215.52 220.8421"
   version="1.1"
   id="svg1242"
   sodipodi:docname="5.svg"
   inkscape:version="1.0.1 (c497b03c, 2020-09-10)"
   width="57.022999mm"
   height="58.431137mm">
  <metadata
     id="metadata1246">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title>Marketing_strategy_SVG</dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1440"
     inkscape:window-height="635"
     id="namedview1244"
     showgrid="false"
     inkscape:zoom="0.49908293"
     inkscape:cx="-364.03258"
     inkscape:cy="111.25926"
     inkscape:window-x="0"
     inkscape:window-y="25"
     inkscape:window-maximized="0"
     inkscape:current-layer="Слой_1-2"
     inkscape:document-rotation="0"
     units="mm"
     fit-margin-top="0"
     fit-margin-left="0"
     fit-margin-right="0"
     fit-margin-bottom="0" />
  <defs
     id="defs835">
    <style
       id="style833">.cls-1,.cls-15,.cls-4{fill:#d6d8e5;}.cls-1{opacity:0.4;}.cls-2{fill:#b1b4c4;}.cls-3{fill:#9ea2b2;}.cls-5{fill:#f4f4f4;}.cls-6{fill:#9acc12;}.cls-7{fill:#e8bc05;}.cls-8{fill:#ef6848;}.cls-9{fill:#be4aed;}.cls-10{fill:#543526;}.cls-11{fill:#1b96ea;}.cls-12{fill:#ff5050;}.cls-13{fill:#32cec3;}.cls-14{fill:none;stroke:#d6d8e5;stroke-miterlimit:10;stroke-width:1.42px;}.cls-15{opacity:0.3;}.cls-16{fill:#dd990e;}.cls-17{fill:#f9cb07;}.cls-18{fill:#cc8b09;}.cls-19{fill:#e8a30a;}.cls-20{fill:#f9ca06;}.cls-21{fill:#3a2c6d;}.cls-22{fill:#ffcea9;}.cls-23{fill:#38226d;}.cls-24{fill:#9c73ff;}.cls-25{fill:#8c50ff;}.cls-26{fill:#ededed;}.cls-27{fill:#d33d3d;}.cls-28{fill:#ff4d4d;}.cls-29{fill:#2b303f;}</style>
  </defs>
  <title
     id="title837">Marketing_strategy_SVG</title>
  <g
     id="Слой_1-2"
     data-name="Слой 1"
     transform="translate(-88.126634,-152.59003)">
    <path
       class="cls-1"
       d="m 278.96663,372.54665 -113.6,-65.58 a 6.38,6.38 0 0 1 0,-11.05 v 0 a 6.38,6.38 0 0 1 6.38,0 l 113.6,65.63 a 6.38,6.38 0 0 1 -0.73,11.41 v 0 a 6.36,6.36 0 0 1 -5.65,-0.41 z"
       id="path1214" />
    <path
       class="cls-27"
       d="m 229.91663,332.87665 c -40.66,-23.47 -73.73,-80.76 -73.73,-127.7 0,-46.94 33.07,-66 73.73,-42.56 40.66,23.44 73.73,80.75 73.73,127.7 0,46.95 -33.08,66.03 -73.73,42.56 z m 0,-155 c -33.38,-19.27 -60.54,-3.59 -60.54,34.95 0,38.54 27.16,85.57 60.54,104.84 33.38,19.27 60.53,3.6 60.53,-34.94 0,-38.54 -27.15,-85.61 -60.53,-104.88 z"
       id="path1216" />
    <polygon
       class="cls-27"
       points="83.82,129.6 97.33,122.22 95.31,131.48 "
       id="polygon1218"
       transform="translate(83.866634,33.546654)" />
    <path
       class="cls-27"
       d="m 269.48663,344.82665 c 0.33,-0.15 14.77,-8.23 14.77,-8.23 l -11.16,-1.79 z"
       id="path1220" />
    <ellipse
       class="cls-5"
       cx="64.47393"
       cy="329.32858"
       rx="54.810001"
       ry="94.940002"
       transform="rotate(-30)"
       id="ellipse1222" />
    <path
       class="cls-28"
       d="m 220.50663,338.10665 c -40.64,-23.48 -73.73,-80.76 -73.73,-127.71 0,-46.95 33.08,-66 73.73,-42.56 40.65,23.44 73.73,80.76 73.73,127.7 0,46.94 -33.07,66.01 -73.73,42.57 z m 0,-155 c -33.37,-19.27 -60.53,-3.6 -60.53,34.94 0,38.54 27.16,85.58 60.53,104.85 33.37,19.27 60.54,3.59 60.54,-34.95 0,-38.54 -27.18,-85.6 -60.54,-104.87 z"
       id="path1224" />
    <path
       class="cls-28"
       d="m 220.50663,305.70665 c -25.18,-14.54 -45.64,-50.03 -45.64,-79.11 0,-29.08 20.49,-40.91 45.67,-26.37 25.18,14.54 45.68,50 45.68,79.11 0,29.11 -20.52,40.92 -45.71,26.37 z m 0,-90.24 c -17.91,-10.34 -32.48,-1.93 -32.48,18.75 0,20.68 14.57,45.92 32.48,56.26 17.91,10.34 32.48,1.92 32.48,-18.76 0,-20.68 -14.57,-45.91 -32.48,-56.25 z"
       id="path1226" />
    <path
       class="cls-28"
       d="m 220.50663,273.82665 c -10,-5.75 -18.06,-19.79 -18.06,-31.29 0,-11.5 8.1,-16.18 18.06,-10.43 9.96,5.75 18.07,19.79 18.07,31.29 0,11.5 -8.1,16.15 -18.07,10.43 z m 0,-26.48 c -2.68,-1.55 -4.87,-0.29 -4.87,2.81 a 10.79,10.79 0 0 0 4.87,8.43 c 2.69,1.55 4.87,0.29 4.87,-2.81 a 10.76,10.76 0 0 0 -4.87,-8.43 z"
       id="path1228" />
    <polygon
       class="cls-29"
       points="26.22,281.67 13.49,289.58 11.02,290.9 4.26,290.99 "
       id="polygon1230"
       transform="translate(83.866634,33.546654)" />
    <ellipse
       class="cls-19"
       cx="-79.061852"
       cy="330.15607"
       rx="1.41"
       ry="2.4400001"
       transform="rotate(-30)"
       id="ellipse1232" />
    <ellipse
       class="cls-19"
       cx="64.443764"
       cy="330.02509"
       rx="1.41"
       ry="2.4400001"
       transform="rotate(-30)"
       id="ellipse1234" />
    <polygon
       class="cls-29"
       points="31.69,282.7 30.92,284.29 15.64,300.69 14.03,293.96 "
       id="polygon1236"
       transform="translate(83.866634,33.546654)" />
    <path
       class="cls-19"
       d="m 97.866634,327.54665 c 0.15,-0.13 124.149996,-71.81 124.149996,-71.81 l -2.28,-4.3 -124.379996,72 z"
       id="path1238" />
  </g>
</svg>

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/backends/remote_backend.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Remote Backend Implementation

This backend uses the existing jupyter_nbmodel_client, jupyter_kernel_client,
and jupyter_server_api packages to connect to remote Jupyter servers.

For MCP_SERVER mode, this maintains 100% backward compatibility with the existing implementation.
"""

from typing import Optional, Any, Union, Literal
from mcp.types import ImageContent
from jupyter_mcp_server.jupyter_extension.backends.base import Backend

# Note: This is a placeholder that delegates to existing server.py logic
# The actual implementation will be refactored from server.py in a later step
# For now, this establishes the pattern


class RemoteBackend(Backend):
    """
    Backend that connects to remote Jupyter servers using HTTP/WebSocket APIs.
    
    Uses:
    - jupyter_nbmodel_client.NbModelClient for notebook operations
    - jupyter_kernel_client.KernelClient for kernel operations  
    - jupyter_server_api.JupyterServerClient for server operations
    """
    
    def __init__(self, document_url: str, document_token: str, runtime_url: str, runtime_token: str):
        """
        Initialize remote backend.
        
        Args:
            document_url: URL of Jupyter server for document operations
            document_token: Authentication token for document server
            runtime_url: URL of Jupyter server for runtime operations
            runtime_token: Authentication token for runtime server
        """
        self.document_url = document_url
        self.document_token = document_token
        self.runtime_url = runtime_url
        self.runtime_token = runtime_token
    
    # Notebook operations
    
    async def get_notebook_content(self, path: str) -> dict[str, Any]:
        """Get notebook content via remote API."""
        # TODO: Implement using jupyter_server_api
        raise NotImplementedError("To be refactored from server.py")
    
    async def list_notebooks(self, path: str = "") -> list[str]:
        """List notebooks via remote API."""
        # TODO: Implement using jupyter_server_api
        raise NotImplementedError("To be refactored from server.py")
    
    async def notebook_exists(self, path: str) -> bool:
        """Check if notebook exists via remote API."""
        # TODO: Implement using jupyter_server_api
        raise NotImplementedError("To be refactored from server.py")
    
    async def create_notebook(self, path: str) -> dict[str, Any]:
        """Create notebook via remote API."""
        # TODO: Implement using jupyter_server_api
        raise NotImplementedError("To be refactored from server.py")
    
    # Cell operations
    
    async def read_cells(
        self, 
        path: str, 
        start_index: Optional[int] = None,
        end_index: Optional[int] = None
    ) -> list[dict[str, Any]]:
        """Read cells via nbmodel_client."""
        # TODO: Implement using jupyter_nbmodel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def append_cell(
        self, 
        path: str, 
        cell_type: Literal["code", "markdown"],
        source: Union[str, list[str]]
    ) -> int:
        """Append cell via nbmodel_client."""
        # TODO: Implement using jupyter_nbmodel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def insert_cell(
        self,
        path: str,
        cell_index: int,
        cell_type: Literal["code", "markdown"],
        source: Union[str, list[str]]
    ) -> int:
        """Insert cell via nbmodel_client."""
        # TODO: Implement using jupyter_nbmodel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def delete_cell(self, path: str, cell_index: int) -> None:
        """Delete cell via nbmodel_client."""
        # TODO: Implement using jupyter_nbmodel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def overwrite_cell(
        self,
        path: str,
        cell_index: int,
        new_source: Union[str, list[str]]
    ) -> tuple[str, str]:
        """Overwrite cell via nbmodel_client."""
        # TODO: Implement using jupyter_nbmodel_client
        raise NotImplementedError("To be refactored from server.py")
    
    # Kernel operations
    
    async def get_or_create_kernel(self, path: str, kernel_id: Optional[str] = None) -> str:
        """Get or create kernel via kernel_client."""
        # TODO: Implement using jupyter_kernel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def execute_cell(
        self,
        path: str,
        cell_index: int,
        kernel_id: str,
        timeout_seconds: int = 300
    ) -> list[Union[str, ImageContent]]:
        """Execute cell via kernel_client."""
        # TODO: Implement using jupyter_kernel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def interrupt_kernel(self, kernel_id: str) -> None:
        """Interrupt kernel via kernel_client."""
        # TODO: Implement using jupyter_kernel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def restart_kernel(self, kernel_id: str) -> None:
        """Restart kernel via kernel_client."""
        # TODO: Implement using jupyter_kernel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def shutdown_kernel(self, kernel_id: str) -> None:
        """Shutdown kernel via kernel_client."""
        # TODO: Implement using jupyter_kernel_client
        raise NotImplementedError("To be refactored from server.py")
    
    async def list_kernels(self) -> list[dict[str, Any]]:
        """List kernels via server API."""
        # TODO: Implement using jupyter_server_api
        raise NotImplementedError("To be refactored from server.py")
    
    async def kernel_exists(self, kernel_id: str) -> bool:
        """Check if kernel exists via server API."""
        # TODO: Implement using jupyter_server_api
        raise NotImplementedError("To be refactored from server.py")

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/backends/base.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""
Abstract Backend Interface

Defines the contract for all backend implementations (Remote and Local).
"""

from abc import ABC, abstractmethod
from typing import Optional, Any, Union, Literal
from mcp.types import ImageContent


class Backend(ABC):
    """
    Abstract backend for notebook and kernel operations.
    
    Implementations:
    - RemoteBackend: Uses jupyter_nbmodel_client, jupyter_kernel_client, jupyter_server_api
    - LocalBackend: Uses local serverapp.contents_manager and serverapp.kernel_manager
    """
    
    # Notebook operations
    
    @abstractmethod
    async def get_notebook_content(self, path: str) -> dict[str, Any]:
        """
        Retrieve notebook content.
        
        Args:
            path: Path to the notebook file
            
        Returns:
            Dictionary with notebook content (cells, metadata)
        """
        pass
    
    @abstractmethod
    async def list_notebooks(self, path: str = "") -> list[str]:
        """
        List all notebooks in a directory.
        
        Args:
            path: Directory path (empty string for root)
            
        Returns:
            List of notebook paths
        """
        pass
    
    @abstractmethod
    async def notebook_exists(self, path: str) -> bool:
        """
        Check if a notebook exists.
        
        Args:
            path: Path to the notebook file
            
        Returns:
            True if notebook exists
        """
        pass
    
    @abstractmethod
    async def create_notebook(self, path: str) -> dict[str, Any]:
        """
        Create a new notebook.
        
        Args:
            path: Path for the new notebook
            
        Returns:
            Created notebook content
        """
        pass
    
    # Cell operations (via notebook connection)
    
    @abstractmethod
    async def read_cells(
        self, 
        path: str, 
        start_index: Optional[int] = None,
        end_index: Optional[int] = None
    ) -> list[dict[str, Any]]:
        """
        Read cells from a notebook.
        
        Args:
            path: Notebook path
            start_index: Start cell index (None for all)
            end_index: End cell index (None for all)
            
        Returns:
            List of cell dictionaries
        """
        pass
    
    @abstractmethod
    async def append_cell(
        self, 
        path: str, 
        cell_type: Literal["code", "markdown"],
        source: Union[str, list[str]]
    ) -> int:
        """
        Append a cell to notebook.
        
        Args:
            path: Notebook path
            cell_type: Type of cell
            source: Cell source code/markdown
            
        Returns:
            Index of appended cell
        """
        pass
    
    @abstractmethod
    async def insert_cell(
        self,
        path: str,
        cell_index: int,
        cell_type: Literal["code", "markdown"],
        source: Union[str, list[str]]
    ) -> int:
        """
        Insert a cell at specific index.
        
        Args:
            path: Notebook path
            cell_index: Where to insert
            cell_type: Type of cell
            source: Cell source
            
        Returns:
            Index of inserted cell
        """
        pass
    
    @abstractmethod
    async def delete_cell(self, path: str, cell_index: int) -> None:
        """
        Delete a cell from notebook.
        
        Args:
            path: Notebook path
            cell_index: Index of cell to delete
        """
        pass
    
    @abstractmethod
    async def overwrite_cell(
        self,
        path: str,
        cell_index: int,
        new_source: Union[str, list[str]]
    ) -> tuple[str, str]:
        """
        Overwrite cell content.
        
        Args:
            path: Notebook path
            cell_index: Index of cell to overwrite
            new_source: New source content
            
        Returns:
            Tuple of (old_source, new_source) for diff generation
        """
        pass
    
    # Kernel operations
    
    @abstractmethod
    async def get_or_create_kernel(self, path: str, kernel_id: Optional[str] = None) -> str:
        """
        Get existing kernel or create new one for a notebook.
        
        Args:
            path: Notebook path
            kernel_id: Specific kernel ID (None to create new)
            
        Returns:
            Kernel ID
        """
        pass
    
    @abstractmethod
    async def execute_cell(
        self,
        path: str,
        cell_index: int,
        kernel_id: str,
        timeout_seconds: int = 300
    ) -> list[Union[str, ImageContent]]:
        """
        Execute a cell and return outputs.
        
        Args:
            path: Notebook path
            cell_index: Index of cell to execute
            kernel_id: Kernel to use
            timeout_seconds: Execution timeout
            
        Returns:
            List of cell outputs
        """
        pass
    
    @abstractmethod
    async def interrupt_kernel(self, kernel_id: str) -> None:
        """
        Interrupt a running kernel.
        
        Args:
            kernel_id: Kernel to interrupt
        """
        pass
    
    @abstractmethod
    async def restart_kernel(self, kernel_id: str) -> None:
        """
        Restart a kernel.
        
        Args:
            kernel_id: Kernel to restart
        """
        pass
    
    @abstractmethod
    async def shutdown_kernel(self, kernel_id: str) -> None:
        """
        Shutdown a kernel.
        
        Args:
            kernel_id: Kernel to shutdown
        """
        pass
    
    @abstractmethod
    async def list_kernels(self) -> list[dict[str, Any]]:
        """
        List all running kernels.
        
        Returns:
            List of kernel information dictionaries
        """
        pass
    
    @abstractmethod
    async def kernel_exists(self, kernel_id: str) -> bool:
        """
        Check if a kernel exists.
        
        Args:
            kernel_id: Kernel ID to check
            
        Returns:
            True if kernel exists
        """
        pass

```

--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/list_notebooks_tool.py:
--------------------------------------------------------------------------------

```python
# Copyright (c) 2023-2024 Datalayer, Inc.
#
# BSD 3-Clause License

"""List notebooks tool implementation."""

from typing import Any, Optional, List
from jupyter_server_api import JupyterServerClient
from jupyter_mcp_server.tools._base import BaseTool, ServerMode
from jupyter_mcp_server.notebook_manager import NotebookManager
from jupyter_mcp_server.utils import format_TSV


class ListNotebooksTool(BaseTool):
    """Tool to list all notebooks in the Jupyter server."""
    
    @property
    def name(self) -> str:
        return "list_notebooks"
    
    @property
    def description(self) -> str:
        return """List all notebooks in the Jupyter server (including subdirectories) and show which ones are managed.
    
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.

Returns:
    str: TSV formatted table with notebook information including management status"""
    
    def _list_notebooks_http(self, server_client: JupyterServerClient, path: str = "", notebooks: Optional[List[str]] = None) -> List[str]:
        """List notebooks using HTTP API (MCP_SERVER mode)."""
        if notebooks is None:
            notebooks = []
        
        try:
            contents = server_client.contents.list_directory(path)
            for item in contents:
                full_path = f"{path}/{item.name}" if path else item.name
                if item.type == "directory":
                    # Recursively search subdirectories
                    self._list_notebooks_http(server_client, full_path, notebooks)
                elif item.type == "notebook" or (item.type == "file" and item.name.endswith('.ipynb')):
                    # Add notebook to list without any prefix
                    notebooks.append(full_path)
        except Exception as e:
            # If we can't access a directory, just skip it
            pass
        
        return notebooks
    
    async def _list_notebooks_local(self, contents_manager: Any, path: str = "", notebooks: Optional[List[str]] = None) -> List[str]:
        """List notebooks using local contents_manager API (JUPYTER_SERVER mode)."""
        if notebooks is None:
            notebooks = []
        
        try:
            model = await contents_manager.get(path, content=True, type='directory')
            for item in model.get('content', []):
                full_path = f"{path}/{item['name']}" if path else item['name']
                if item['type'] == "directory":
                    # Recursively search subdirectories
                    await self._list_notebooks_local(contents_manager, full_path, notebooks)
                elif item['type'] == "notebook" or (item['type'] == "file" and item['name'].endswith('.ipynb')):
                    # Add notebook to list
                    notebooks.append(full_path)
        except Exception as e:
            # If we can't access a directory, just skip it
            pass
        
        return notebooks
    
    async def execute(
        self,
        mode: ServerMode,
        server_client: Optional[JupyterServerClient] = None,
        kernel_client: Optional[Any] = None,
        contents_manager: Optional[Any] = None,
        kernel_manager: Optional[Any] = None,
        kernel_spec_manager: Optional[Any] = None,
        notebook_manager: Optional[NotebookManager] = None,
        **kwargs
    ) -> str:
        """Execute the list_notebook tool.
        
        Args:
            mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
            server_client: HTTP client for MCP_SERVER mode
            contents_manager: Direct API access for JUPYTER_SERVER mode
            notebook_manager: Notebook manager instance
            **kwargs: Additional parameters (unused)
            
        Returns:
            TSV formatted table with notebook information
        """
        # Get all notebooks based on mode
        if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
            all_notebooks = await self._list_notebooks_local(contents_manager)
        elif mode == ServerMode.MCP_SERVER and server_client is not None:
            all_notebooks = self._list_notebooks_http(server_client)
        else:
            raise ValueError(f"Invalid mode or missing required clients: mode={mode}")
        
        # Get managed notebooks info
        managed_notebooks = notebook_manager.list_all_notebooks() if notebook_manager else {}
        
        if not all_notebooks and not managed_notebooks:
            return "No notebooks found in the Jupyter server."
        
        # Create TSV formatted output
        headers = ["Path", "Managed", "Name", "Status", "Current"]
        rows = []
        
        # Create a set of managed notebook paths for quick lookup
        managed_paths = {info["path"] for info in managed_notebooks.values()}
        
        # Add all notebooks found in the server
        for notebook_path in sorted(all_notebooks):
            is_managed = notebook_path in managed_paths
            
            if is_managed:
                # Find the managed notebook entry
                managed_info = None
                managed_name = None
                for name, info in managed_notebooks.items():
                    if info["path"] == notebook_path:
                        managed_info = info
                        managed_name = name
                        break
                
                if managed_info:
                    current_marker = "✓" if managed_info["is_current"] else ""
                    rows.append([notebook_path, "Yes", managed_name, managed_info['kernel_status'], current_marker])
                else:
                    rows.append([notebook_path, "Yes", "-", "-", ""])
            else:
                rows.append([notebook_path, "No", "-", "-", ""])
        
        # Add any managed notebooks that weren't found in the server (edge case)
        for name, info in managed_notebooks.items():
            if info["path"] not in all_notebooks:
                current_marker = "✓" if info["is_current"] else ""
                rows.append([info['path'], "Yes (not found)", name, info['kernel_status'], current_marker])
        
        return format_TSV(headers, rows)

```
Page 1/5FirstPrevNextLast