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 --> [](https://datalayer.io) [](https://github.com/sponsors/datalayer) ``` -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- ```markdown <!-- ~ Copyright (c) 2023-2024 Datalayer, Inc. ~ ~ BSD 3-Clause License --> [](https://datalayer.io) [](https://github.com/sponsors/datalayer) ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- ```markdown <!-- ~ Copyright (c) 2023-2024 Datalayer, Inc. ~ ~ BSD 3-Clause License --> [](https://datalayer.io) [](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 --> [](https://datalayer.io) [](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)* [](https://pypi.org/project/jupyter-mcp-server) [](https://www.python.org/downloads/) [](https://hub.docker.com/r/datalayer/jupyter-mcp-server) [](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)  </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  ## 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  ## 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  ## 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  ## 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'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.  ``` -------------------------------------------------------------------------------- /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) ```