├── docs
├── docs
│ ├── _category_.yaml
│ ├── prompts
│ │ ├── _category_.yaml
│ │ └── index.mdx
│ ├── tools
│ │ └── _category_.yaml
│ ├── clients
│ │ ├── _category_.yaml
│ │ ├── cline
│ │ │ ├── _category_.yaml
│ │ │ └── index.mdx
│ │ ├── cursor
│ │ │ ├── _category_.yaml
│ │ │ └── index.mdx
│ │ ├── vscode
│ │ │ ├── _category_.yaml
│ │ │ └── index.mdx
│ │ ├── windsurf
│ │ │ ├── _category_.yaml
│ │ │ └── index.mdx
│ │ ├── claude_desktop
│ │ │ ├── _category_.yaml
│ │ │ └── index.mdx
│ │ └── index.mdx
│ ├── releases
│ │ ├── _category_.yaml
│ │ └── index.mdx
│ ├── contribute
│ │ ├── _category_.yaml
│ │ └── index.mdx
│ ├── resources
│ │ ├── _category_.yaml
│ │ └── index.mdx
│ ├── configuration
│ │ └── _category_.yaml
│ ├── jupyter
│ │ ├── _category_.yaml
│ │ ├── stdio
│ │ │ └── _category_.yaml
│ │ ├── streamable-http
│ │ │ ├── _category_.yaml
│ │ │ ├── jupyter-extension
│ │ │ │ └── index.mdx
│ │ │ └── standalone
│ │ │ │ └── index.mdx
│ │ └── index.mdx
│ ├── datalayer
│ │ ├── _category_.yaml
│ │ └── streamable-http
│ │ │ └── index.mdx
│ ├── getting_started
│ │ ├── _category_.yaml
│ │ └── index.mdx
│ └── index.mdx
├── static
│ └── img
│ │ ├── favicon.ico
│ │ ├── datalayer
│ │ ├── logo.png
│ │ └── logo.svg
│ │ ├── feature_2.svg
│ │ └── feature_1.svg
├── babel.config.js
├── src
│ ├── pages
│ │ ├── markdown-page.md
│ │ ├── testimonials.tsx
│ │ └── index.module.css
│ ├── components
│ │ ├── HomepageFeatures.module.css
│ │ ├── HomepageProducts.module.css
│ │ ├── HomepageFeatures.js
│ │ └── HomepageProducts.js
│ └── theme
│ │ └── CustomDocItem.tsx
├── .gitignore
├── .yarnrc.yml
├── sidebars.js
├── README.md
├── LICENSE
├── package.json
├── Makefile
└── docusaurus.config.js
├── tests
├── __init__.py
├── test_config.py
├── test_prompts.py
└── test_jupyter_extension.py
├── pytest.ini
├── jupyter_mcp_server
├── __version__.py
├── jupyter_extension
│ ├── backends
│ │ ├── __init__.py
│ │ ├── remote_backend.py
│ │ └── base.py
│ ├── protocol
│ │ ├── __init__.py
│ │ └── messages.py
│ ├── __init__.py
│ └── context.py
├── log.py
├── __main__.py
├── __init__.py
├── tools
│ ├── _base.py
│ ├── __init__.py
│ ├── list_notebooks_tool.py
│ ├── read_notebook_tool.py
│ ├── read_cell_tool.py
│ ├── restart_notebook_tool.py
│ └── unuse_notebook_tool.py
├── server_modes.py
├── enroll.py
├── models.py
├── config.py
├── server_context.py
└── tool_cache.py
├── jupyter-config
├── jupyter_notebook_config
│ └── jupyter_mcp_server.json
└── jupyter_server_config.d
│ └── jupyter_mcp_server.json
├── .vscode
├── settings.json
└── mcp.json
├── .github
├── workflows
│ ├── lint.sh
│ ├── test.yml
│ ├── fix-license-header.yml
│ ├── build.yml
│ └── release.yml
└── dependabot.yml
├── .dockerignore
├── .licenserc.yaml
├── dev
├── README.md
└── content
│ ├── README.md
│ └── new.ipynb
├── Dockerfile
├── smithery.yaml
├── .pre-commit-config.yaml
├── LICENSE
├── prompt
├── README.md
└── general
│ ├── README.md
│ └── AGENT.md
├── .gitignore
├── pyproject.toml
├── CODE_OF_CONDUCT.md
├── RELEASE.md
├── Makefile
└── CONTRIBUTING.md
/docs/docs/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Overview"
2 | position: 1
3 |
--------------------------------------------------------------------------------
/docs/docs/prompts/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Prompts"
2 | position: 6
--------------------------------------------------------------------------------
/docs/docs/tools/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Tools"
2 | position: 5
3 |
--------------------------------------------------------------------------------
/docs/docs/clients/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Clients"
2 | position: 7
3 |
--------------------------------------------------------------------------------
/docs/docs/releases/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Releases"
2 | position: 11
3 |
--------------------------------------------------------------------------------
/docs/docs/clients/cline/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Cline"
2 | position: 4
3 |
--------------------------------------------------------------------------------
/docs/docs/clients/cursor/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Cursor"
2 | position: 3
3 |
--------------------------------------------------------------------------------
/docs/docs/clients/vscode/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "VS Code"
2 | position: 2
3 |
--------------------------------------------------------------------------------
/docs/docs/contribute/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Contribute"
2 | position: 9
3 |
--------------------------------------------------------------------------------
/docs/docs/resources/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Resources"
2 | position: 12
3 |
--------------------------------------------------------------------------------
/docs/docs/clients/windsurf/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Windsurf"
2 | position: 5
3 |
--------------------------------------------------------------------------------
/docs/docs/configuration/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Configuration"
2 | position: 5
3 |
--------------------------------------------------------------------------------
/docs/docs/jupyter/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Jupyter Notebooks"
2 | position: 3
3 |
--------------------------------------------------------------------------------
/docs/docs/datalayer/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Datalayer Notebooks"
2 | position: 4
3 |
--------------------------------------------------------------------------------
/docs/docs/getting_started/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Getting Started"
2 | position: 2
3 |
--------------------------------------------------------------------------------
/docs/docs/jupyter/stdio/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "STDIO Transport"
2 | position: 1
3 |
--------------------------------------------------------------------------------
/docs/docs/clients/claude_desktop/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Claude Desktop"
2 | position: 1
3 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
--------------------------------------------------------------------------------
/docs/docs/jupyter/streamable-http/_category_.yaml:
--------------------------------------------------------------------------------
1 | label: "Streamable HTTP Transport"
2 | position: 2
3 |
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datalayer/jupyter-mcp-server/HEAD/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/datalayer/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datalayer/jupyter-mcp-server/HEAD/docs/static/img/datalayer/logo.png
--------------------------------------------------------------------------------
/docs/docs/datalayer/streamable-http/index.mdx:
--------------------------------------------------------------------------------
1 | # Streamable HTTP Transport
2 |
3 | :::warning
4 | Documentation under construction.
5 | :::
6 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2024- Datalayer, Inc.
2 | ;
3 | ; BSD 3-Clause License
4 |
5 | [pytest]
6 | addopts = -rqA
7 | log_cli = true
8 | log_level = INFO
9 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/__version__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Jupyter MCP Server."""
6 |
7 | __version__ = "0.21.1"
8 |
--------------------------------------------------------------------------------
/jupyter-config/jupyter_notebook_config/jupyter_mcp_server.json:
--------------------------------------------------------------------------------
1 | {
2 | "ServerApp": {
3 | "nbserver_extensions": {
4 | "jupyter_mcp_server": true
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/jupyter-config/jupyter_server_config.d/jupyter_mcp_server.json:
--------------------------------------------------------------------------------
1 | {
2 | "ServerApp": {
3 | "jpserver_extensions": {
4 | "jupyter_mcp_server": true
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/backends/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Backend implementations for notebook and kernel operations"""
6 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/protocol/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """MCP Protocol implementation for Jupyter Server extension"""
6 |
--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | module.exports = {
8 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
9 | };
10 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/log.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Logging Configuration for Jupyter MCP Server"""
6 |
7 | import logging
8 |
9 | logger = logging.getLogger(__name__)
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python-envs.pythonProjects": [
3 | {
4 | "path": "",
5 | "envManager": "ms-python.python:conda",
6 | "packageManager": "ms-python.python:conda"
7 | }
8 | ]
9 | }
--------------------------------------------------------------------------------
/docs/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ---
8 | title: Markdown page example
9 | ---
10 |
11 | # Markdown page example
12 |
13 | You don't need React to write simple standalone pages.
14 |
--------------------------------------------------------------------------------
/.github/workflows/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Copyright (c) 2024- Datalayer, Inc.
3 | #
4 | # BSD 3-Clause License
5 |
6 | pip install -e ".[lint,typing]"
7 | mypy --install-types --non-interactive .
8 | ruff check .
9 | mdformat --check *.md
10 | pipx run 'validate-pyproject[all]' pyproject.toml
11 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | *.pyo
4 | *.pyd
5 | .Python
6 | env
7 | pip-log.txt
8 | pip-delete-this-directory.txt
9 | .tox
10 | .coverage
11 | .coverage.*
12 | .cache
13 | nosetests.xml
14 | coverage.xml
15 | *.cover
16 | *.log
17 | .git
18 | .github
19 | .mypy_cache
20 | .pytest_cache
21 | dev
22 | docs
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | *.lock
23 |
--------------------------------------------------------------------------------
/.licenserc.yaml:
--------------------------------------------------------------------------------
1 | header:
2 | license:
3 | content: |
4 | Copyright (c) 2024- Datalayer, Inc.
5 |
6 | BSD 3-Clause License
7 |
8 |
9 | paths-ignore:
10 | - '**/*.ipynb'
11 | - '**/*.json'
12 | - '**/*.yaml'
13 | - '**/*.yml'
14 | - '**/.*'
15 | - 'docs/**/*'
16 | - 'LICENSE'
17 |
18 | comment: on-failure
--------------------------------------------------------------------------------
/jupyter_mcp_server/__main__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Entry point for running jupyter_mcp_server as a module.
7 |
8 | This allows the package to be run with: python -m jupyter_mcp_server
9 | """
10 |
11 | from jupyter_mcp_server.CLI import server
12 |
13 | if __name__ == "__main__":
14 | server()
15 |
16 |
--------------------------------------------------------------------------------
/dev/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | [](https://datalayer.io)
8 |
9 | [](https://github.com/sponsors/datalayer)
10 |
--------------------------------------------------------------------------------
/dev/content/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | [](https://datalayer.io)
8 |
9 | [](https://github.com/sponsors/datalayer)
10 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures.module.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | /* stylelint-disable docusaurus/copyright-header */
8 |
9 | .features {
10 | display: flex;
11 | align-items: center;
12 | padding: 2rem 0;
13 | width: 100%;
14 | }
15 |
16 | .featureSvg {
17 | height: 200px;
18 | width: 200px;
19 | }
20 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageProducts.module.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | /* stylelint-disable docusaurus/copyright-header */
8 |
9 | .product {
10 | display: flex;
11 | align-items: center;
12 | padding: 2rem 0;
13 | width: 100%;
14 | }
15 |
16 | .productSvg {
17 | height: 200px;
18 | width: 200px;
19 | }
20 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Jupyter MCP Server."""
6 |
7 | from jupyter_mcp_server.__version__ import __version__
8 | from jupyter_mcp_server.jupyter_extension.extension import _jupyter_server_extension_points
9 |
10 |
11 | __all__ = [
12 | "__version__",
13 | "_jupyter_server_extension_points",
14 | ]
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | groups:
8 | actions:
9 | patterns:
10 | - "*"
11 | - package-ecosystem: "pip"
12 | directory: "/"
13 | schedule:
14 | interval: "monthly"
15 | groups:
16 | pip:
17 | patterns:
18 | - "*"
19 |
--------------------------------------------------------------------------------
/docs/src/theme/CustomDocItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ThemeProvider } from '@primer/react-brand';
3 | import DocItem from "@theme/DocItem";
4 |
5 | import '@primer/react-brand/lib/css/main.css'
6 |
7 | export const CustomDocItem = (props: any) => {
8 | return (
9 | <>
10 |
11 |
12 |
13 | >
14 | )
15 | }
16 |
17 | export default CustomDocItem;
18 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Jupyter to MCP Adapter Package
7 |
8 | This package provides the adapter layer to expose MCP server tools as a Jupyter Server extension.
9 | It supports dual-mode operation: standalone MCP server and embedded Jupyter server extension.
10 | """
11 |
12 | from jupyter_mcp_server.jupyter_extension.context import ServerContext, get_server_context
13 |
14 | __all__ = ["ServerContext", "get_server_context"]
15 |
--------------------------------------------------------------------------------
/docs/docs/clients/windsurf/index.mdx:
--------------------------------------------------------------------------------
1 | # Windsurf
2 |
3 | 
4 |
5 | ## Install Windsurf
6 |
7 | Install the Windsurf app from the [Windsurf website](https://windsurf.com/download).
8 |
9 | ## Configure Jupyter MCP Server
10 |
11 | To use Jupyter MCP Server with Windsurf, add the [Jupyter MCP Server configuration](/jupyter/stdio#2-setup-jupyter-mcp-server) to your `mcp_config.json` file, read more on the [MCP Windsurf documentation website](https://docs.windsurf.com/windsurf/cascade/mcp).
12 |
--------------------------------------------------------------------------------
/docs/docs/clients/cursor/index.mdx:
--------------------------------------------------------------------------------
1 | # Cursor
2 |
3 | 
4 |
5 | ## Install Cursor
6 |
7 | Install the Cursor app from the [Cursor website](https://www.cursor.com/downloads).
8 |
9 | ## Configure Jupyter MCP Server
10 |
11 | To use Jupyter MCP Server with Cursor, add the [Jupyter MCP Server configuration](/jupyter/stdio#2-setup-jupyter-mcp-server) to your `.cursor/mcp.json` file, read more on the [MCP Cursor documentation website](https://docs.cursor.com/context/model-context-protocol#configuring-mcp-servers).
12 |
--------------------------------------------------------------------------------
/docs/docs/contribute/index.mdx:
--------------------------------------------------------------------------------
1 | # Contribute
2 |
3 | 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).
4 |
5 | We welcome contributions of all kinds, including:
6 | - 🐛 Bug fixes
7 | - 📝 Improvements to existing features or documentation
8 | - ✨ New feature development
9 |
10 | Your contributions help us improve the project and make it more useful for everyone!
11 |
12 | See more in [CONTRIBUTING.md](https://github.com/datalayer/jupyter-mcp-server/blob/main/CONTRIBUTING.md)
13 |
--------------------------------------------------------------------------------
/docs/docs/clients/cline/index.mdx:
--------------------------------------------------------------------------------
1 | # Cline
2 |
3 | 
4 |
5 | ## Install Cline VS Code extension
6 |
7 | Install the Cline VS Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev).
8 |
9 | ## Configure Jupyter MCP Server
10 |
11 | To use Jupyter MCP Server with Cline, add the [Jupyter MCP Server configuration](/jupyter/stdio#2-setup-jupyter-mcp-server) to your `.cline_mcp_settings.json` file, read more on the [Cline documentation](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev).
12 |
--------------------------------------------------------------------------------
/docs/src/pages/testimonials.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | import React from 'react';
8 | import Layout from '@theme/Layout';
9 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
10 | import HomepageFeatures from '../components/HomepageFeatures';
11 |
12 | export default function Home() {
13 | const {siteConfig} = useDocusaurusContext();
14 | return (
15 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/docs/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) Datalayer, Inc. https://datalayer.io
2 | # Distributed under the terms of the MIT License.
3 |
4 | enableImmutableInstalls: false
5 | enableInlineBuilds: false
6 | enableTelemetry: false
7 | httpTimeout: 60000
8 | nodeLinker: node-modules
9 | npmRegistryServer: "https://registry.yarnpkg.com"
10 | checksumBehavior: update
11 |
12 | # This will fix the build error with @lerna/legacy-package-management
13 | # See https://github.com/lerna/repro/pull/11
14 | packageExtensions:
15 | "@lerna/legacy-package-management@*":
16 | dependencies:
17 | "@lerna/child-process": "*"
18 | "js-yaml": "*"
19 | "rimraf": "*"
20 | peerDependencies:
21 | "nx": "*"
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | FROM python:3.12-slim
6 |
7 | WORKDIR /app
8 |
9 | COPY pyproject.toml LICENSE README.md ./
10 | COPY jupyter_mcp_server/ jupyter_mcp_server/
11 | COPY jupyter-config/ jupyter-config/
12 |
13 | ENV PIP_NO_CACHE_DIR=1 \
14 | PIP_DEFAULT_TIMEOUT=120 \
15 | PIP_DISABLE_PIP_VERSION_CHECK=1
16 | RUN python -m pip install --upgrade pip wheel setuptools && pip --version
17 |
18 | RUN pip install --no-cache-dir -e . && \
19 | pip uninstall -y pycrdt datalayer_pycrdt && \
20 | pip install --no-cache-dir datalayer_pycrdt==0.12.17
21 |
22 | EXPOSE 4040
23 |
24 | ENTRYPOINT ["python", "-m", "jupyter_mcp_server"]
25 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | /**
8 | * Creating a sidebar enables you to:
9 | - create an ordered group of docs
10 | - render a sidebar for each doc of that group
11 | - provide next/previous navigation
12 |
13 | The sidebars can be generated from the filesystem, or explicitly defined here.
14 |
15 | Create as many sidebars as you want.
16 | */
17 |
18 | // @ts-check
19 |
20 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
21 | const sidebars = {
22 | jupyterMCPServerSidebar: [
23 | {
24 | type: 'autogenerated',
25 | dirName: '.',
26 | },
27 | ]
28 | };
29 |
30 | module.exports = sidebars;
31 |
--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | /* stylelint-disable docusaurus/copyright-header */
8 |
9 | /**
10 | * CSS files with the .module.css suffix will be treated as CSS modules
11 | * and scoped locally.
12 | */
13 |
14 | .heroBanner {
15 | padding: 4rem 0;
16 | text-align: center;
17 | position: relative;
18 | overflow: hidden;
19 | }
20 |
21 | @media screen and (max-width: 966px) {
22 | .heroBanner {
23 | padding: 2rem;
24 | }
25 | }
26 |
27 | .buttons {
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | }
32 |
33 | .tag {
34 | font-size: small;
35 | padding: 4px;
36 | border-radius: 5px;
37 | border-width: thick;
38 | border-color: red;
39 | background: orange;
40 | }
41 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | [](https://datalayer.io)
8 |
9 | [](https://github.com/sponsors/datalayer)
10 |
11 | # Jupyter MCP Server Docs
12 |
13 | > Source code for the [Jupyter MCP Server Documentation](https://datalayer.io), built with [Docusaurus](https://docusaurus.io).
14 |
15 | ```bash
16 | # Install the dependencies.
17 | conda install yarn
18 | yarn
19 | ```
20 |
21 | ```bash
22 | # Local Development: This command starts a local development server and opens up a browser window.
23 | # Most changes are reflected live without having to restart the server.
24 | npm start
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/docs/clients/index.mdx:
--------------------------------------------------------------------------------
1 | # Clients
2 |
3 | We have tested and validated the Jupyter MCP Server with the following clients:
4 |
5 | - [Claude Desktop](./claude_desktop)
6 | - [VS Code](./vscode)
7 | - [Cursor](./cursor)
8 | - [Cline](./cline)
9 | - [Windsurf](./windsurf)
10 |
11 | The Jupyter MCP Server is also compatible with **ANY** MCP client — see the growing list in [MCP clients](https://modelcontextprotocol.io/clients). This means that you are **NOT** limited to the clients listed above. Both [STDIO](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) and [streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) transports are supported.
12 |
13 | If you prefer a CLI approach as client, you can use for example the python [mcp-client-cli](https://github.com/adhikasp/mcp-client-cli) package.
14 |
--------------------------------------------------------------------------------
/.vscode/mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "servers": {
3 | // https://github.com/github/github-mcp-server
4 | "Github": {
5 | "url": "https://api.githubcopilot.com/mcp"
6 | },
7 | // This configuration is for Docker on Linux, read https://jupyter-mcp-server.datalayer.tech/clients/
8 | "DatalayerJupyter": {
9 | "command": "docker",
10 | "args": [
11 | "run",
12 | "-i",
13 | "--rm",
14 | "-e",
15 | "DOCUMENT_URL",
16 | "-e",
17 | "DOCUMENT_TOKEN",
18 | "-e",
19 | "DOCUMENT_ID",
20 | "-e",
21 | "RUNTIME_URL",
22 | "-e",
23 | "RUNTIME_TOKEN",
24 | "datalayer/jupyter-mcp-server:latest"
25 | ],
26 | "env": {
27 | "DOCUMENT_URL": "http://host.docker.internal:8888",
28 | "DOCUMENT_TOKEN": "MY_TOKEN",
29 | "DOCUMENT_ID": "notebook.ipynb",
30 | "RUNTIME_URL": "http://host.docker.internal:8888",
31 | "RUNTIME_TOKEN": "MY_TOKEN"
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | defaults:
10 | run:
11 | shell: bash -eux {0}
12 |
13 | jobs:
14 | test:
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | os: [ubuntu-latest, macos-latest, windows-latest]
20 | python-version: ["3.10", "3.13"]
21 |
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v5
25 |
26 | - name: Set up Python ${{ matrix.python-version }}
27 | uses: actions/setup-python@v6
28 | with:
29 | python-version: ${{ matrix.python-version }}
30 |
31 | - name: Install the extension
32 | run: |
33 | python -m pip install ".[test]"
34 | pip uninstall -y pycrdt datalayer_pycrdt
35 | pip install datalayer_pycrdt==0.12.17
36 |
37 | - name: Test the extension
38 | run: |
39 | make test-mcp-server
40 | make test-jupyter-server
41 |
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - serverUrl
10 | - token
11 | - notebookPath
12 | properties:
13 | serverUrl:
14 | type: string
15 | description: The URL of the JupyterLab server that the MCP will connect to.
16 | token:
17 | type: string
18 | description: The token for authenticating with the JupyterLab server.
19 | notebookPath:
20 | type: string
21 | description: The path to the Jupyter notebook to work with.
22 | commandFunction:
23 | # A function that produces the CLI command to start the MCP on stdio.
24 | |-
25 | (config) => ({ command: 'docker', args: ['run', '-i', '--rm', '-e', `DOCUMENT_URL=${config.serverUrl}`, '-e', `TOKEN=${config.token}`, '-e', `DOCUMENT_ID=${config.notebookPath}`, 'datalayer/jupyter-mcp-server:latest'] })
26 |
--------------------------------------------------------------------------------
/docs/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021-2023 Datalayer, Inc.
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autoupdate_schedule: monthly
3 |
4 | repos:
5 | - repo: https://github.com/pre-commit/pre-commit-hooks
6 | rev: v5.0.0
7 | hooks:
8 | - id: end-of-file-fixer
9 | - id: check-case-conflict
10 | - id: check-executables-have-shebangs
11 | - id: requirements-txt-fixer
12 | - id: check-added-large-files
13 | - id: check-case-conflict
14 | - id: check-toml
15 | - id: check-yaml
16 | - id: debug-statements
17 | - id: forbid-new-submodules
18 | - id: check-builtin-literals
19 | - id: trailing-whitespace
20 |
21 | - repo: https://github.com/python-jsonschema/check-jsonschema
22 | rev: 0.29.4
23 | hooks:
24 | - id: check-github-workflows
25 |
26 | - repo: https://github.com/executablebooks/mdformat
27 | rev: 0.7.19
28 | hooks:
29 | - id: mdformat
30 | additional_dependencies:
31 | [mdformat-gfm, mdformat-frontmatter, mdformat-footnote]
32 |
33 | - repo: https://github.com/charliermarsh/ruff-pre-commit
34 | rev: v0.8.0
35 | hooks:
36 | - id: ruff
37 | args: ["--fix"]
38 | - id: ruff-format
39 |
--------------------------------------------------------------------------------
/dev/content/new.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "a70f6bd9",
6 | "metadata": {},
7 | "source": [
8 | "# A New Notebook"
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": 1,
14 | "id": "3c3442e4",
15 | "metadata": {},
16 | "outputs": [
17 | {
18 | "name": "stdout",
19 | "output_type": "stream",
20 | "text": [
21 | "Hello, World!\n"
22 | ]
23 | }
24 | ],
25 | "source": [
26 | "print(\"Hello, World!\")"
27 | ]
28 | },
29 | {
30 | "cell_type": "code",
31 | "execution_count": null,
32 | "id": "68d7d9e9",
33 | "metadata": {},
34 | "outputs": [],
35 | "source": []
36 | }
37 | ],
38 | "metadata": {
39 | "kernelspec": {
40 | "display_name": "base",
41 | "language": "python",
42 | "name": "python3"
43 | },
44 | "language_info": {
45 | "codemirror_mode": {
46 | "name": "ipython",
47 | "version": 3
48 | },
49 | "file_extension": ".py",
50 | "mimetype": "text/x-python",
51 | "name": "python",
52 | "nbconvert_exporter": "python",
53 | "pygments_lexer": "ipython3",
54 | "version": "3.13.5"
55 | }
56 | },
57 | "nbformat": 4,
58 | "nbformat_minor": 5
59 | }
60 |
--------------------------------------------------------------------------------
/docs/docs/clients/claude_desktop/index.mdx:
--------------------------------------------------------------------------------
1 | # Claude Desktop
2 |
3 | 
4 |
5 | ## Install Claude Desktop
6 |
7 | Claude Desktop can be downloaded [from this page](https://claude.ai/download) for macOS and Windows.
8 |
9 | For Linux, we had success using this [UNOFFICIAL build script based on nix](https://github.com/k3d3/claude-desktop-linux-flake)
10 |
11 | ```bash
12 | # ⚠️ UNOFFICIAL
13 | # You can also run `make claude-linux`
14 | NIXPKGS_ALLOW_UNFREE=1 nix run github:k3d3/claude-desktop-linux-flake \
15 | --impure \
16 | --extra-experimental-features flakes \
17 | --extra-experimental-features nix-command
18 | ```
19 |
20 | ## Configure Jupyter MCP Server
21 |
22 | To use Jupyter MCP Server with Claude Desktop, add the [Jupyter MCP Server configuration](/jupyter/stdio#2-setup-jupyter-mcp-server) to your `claude_desktop_config.json` file, read more on the [MCP documentation website](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server).
23 |
24 | **📺 Watch the setup demo**
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@datalayer/jupyter-mcp-server-docs",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids"
15 | },
16 | "dependencies": {
17 | "@datalayer/icons-react": "^1.0.0",
18 | "@datalayer/primer-addons": "^1.0.3",
19 | "@docusaurus/core": "^3.5.2",
20 | "@docusaurus/preset-classic": "^3.5.2",
21 | "@docusaurus/theme-live-codeblock": "^3.5.2",
22 | "@docusaurus/theme-mermaid": "^3.5.2",
23 | "@mdx-js/react": "^3.0.1",
24 | "@primer/react-brand": "^0.58.0",
25 | "clsx": "^2.1.1",
26 | "docusaurus-lunr-search": "^3.5.0",
27 | "react": "18.3.1",
28 | "react-calendly": "^4.1.0",
29 | "react-dom": "18.3.1",
30 | "react-modal-image": "^2.6.0"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.5%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/docs/docs/jupyter/index.mdx:
--------------------------------------------------------------------------------
1 |
2 | # Jupyter Notebooks
3 |
4 | This guide will help you set up a Jupyter MCP Server to connect your preferred MCP client to a JupyterLab instance.
5 | The Jupyter MCP Server acts as a bridge between the MCP client and the JupyterLab server, allowing you to interact with Jupyter notebooks seamlessly.
6 |
7 | You can customize the setup further based on your requirements. Refer to the [server configuration](/configuration) for more details on the possible configurations.
8 |
9 | :::tip JupyterLab Mode
10 |
11 | **New in v0.17.0**: Enable JupyterLab mode for enhanced UI integration! When enabled, notebooks automatically open in JupyterLab and additional UI tools become available. See the [JupyterLab Mode configuration](/configuration#jupyterlab-mode) for details.
12 |
13 | :::
14 |
15 | 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.
16 | For more details on the different transports, refer to the official MCP documentation [here](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports).
17 |
18 | 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).
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2025, Datalayer
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | import React from 'react';
8 | import clsx from 'clsx';
9 | import styles from './HomepageFeatures.module.css';
10 |
11 | const FeatureList = [
12 | /*
13 | {
14 | title: 'Easy to Use',
15 | Svg: require('../../static/img/feature_1.svg').default,
16 | description: (
17 | <>
18 | Datalayer was designed from the ground up to be easily installed and
19 | used to get your data analysis up and running quickly.
20 | >
21 | ),
22 | },
23 | {
24 | title: 'Focus on What Matters',
25 | Svg: require('../../static/img/feature_2.svg').default,
26 | description: (
27 | <>
28 | Datalayer lets you focus on your work, and we'll do the chores.
29 | >
30 | ),
31 | },
32 | {
33 | title: 'Powered by Open Source',
34 | Svg: require('../../static/img/feature_3.svg').default,
35 | description: (
36 | <>
37 | Extend or customize your platform to your needs.
38 | >
39 | ),
40 | },
41 | */
42 | ];
43 |
44 | function Feature({Svg, title, description}) {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
{title}
52 |
{description}
53 |
54 |
55 | );
56 | }
57 |
58 | export default function HomepageFeatures() {
59 | return (
60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => (
64 |
65 | ))}
66 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (c) 2024- Datalayer, Inc.
3 | #
4 | # BSD 3-Clause License
5 |
6 | """
7 | Simple test script to verify the configuration system works correctly.
8 | """
9 |
10 | from jupyter_mcp_server.config import get_config, set_config, reset_config
11 |
12 | def test_config():
13 | """Test the configuration singleton."""
14 | print("Testing Jupyter MCP Configuration System")
15 | print("=" * 50)
16 |
17 | # Test default configuration
18 | config = get_config()
19 | print(f"Default runtime_url: {config.runtime_url}")
20 | print(f"Default document_id: {config.document_id}")
21 | print(f"Default provider: {config.provider}")
22 |
23 | # Test setting configuration
24 | new_config = set_config(
25 | runtime_url="http://localhost:9999",
26 | document_id="test_notebooks.ipynb",
27 | provider="datalayer",
28 | runtime_token="test_token"
29 | )
30 |
31 | print(f"\nUpdated runtime_url: {new_config.runtime_url}")
32 | print(f"Updated document_id: {new_config.document_id}")
33 | print(f"Updated provider: {new_config.provider}")
34 | print(f"Updated runtime_token: {'***' if new_config.runtime_token else 'None'}")
35 |
36 | # Test that singleton works - getting config again should return same values
37 | config2 = get_config()
38 | print(f"\nSingleton test - runtime_url: {config2.runtime_url}")
39 | print(f"Singleton test - document_id: {config2.document_id}")
40 |
41 | # Test reset
42 | reset_config()
43 | config3 = get_config()
44 | print(f"\nAfter reset - runtime_url: {config3.runtime_url}")
45 | print(f"After reset - document_id: {config3.document_id}")
46 | print(f"After reset - provider: {config3.provider}")
47 |
48 | print("\n✅ Configuration system test completed successfully!")
49 |
50 | if __name__ == "__main__":
51 | test_config()
52 |
--------------------------------------------------------------------------------
/docs/docs/prompts/index.mdx:
--------------------------------------------------------------------------------
1 | # Prompts
2 |
3 | The server currently offers 1 prompt for user.
4 |
5 | :::warning
6 |
7 | Not all MCP Clients support the Prompt Feature. You need to ensure that your MCP Client supports it to enable this feature.
8 | Current known MCP Client support status for Prompt:
9 | - Supported: `Claude desktop`, [`Claude Code`](https://docs.claude.com/en/docs/claude-code/mcp#execute-mcp-prompts), [`Gemini CLI`](https://geminicli.com/docs/tools/mcp-server/#invoking-prompts)
10 | - Not supported: `Cursor`
11 |
12 | :::
13 |
14 | ## Jupyter Core Prompt (1 prompt)
15 |
16 | This is the core Prompt component of Jupyter MCP, providing universal and powerful Prompt tools, all prefixed with `jupyter`.
17 |
18 | ### 1. `jupyter-cite`
19 |
20 | This prompt allows users to cite specific cells in a notebook, enabling users to let LLM perform precise subsequent operations on specific cells.
21 |
22 | #### Input Parameters
23 |
24 | - `--prompt`: User prompt for the cited cells
25 | - `--cell_indices`: Cell indices to cite (0-based), supporting flexible range format
26 | 1. **Single Index**: Cite a single cell, such as `"0"` (cites the 1st cell)
27 | 2. **Range Format**: Cite a continuous range of cells, such as `"0-2"` (cites cells 1 to 3)
28 | 3. **Mixed Format**: Combine single index and range, such as `"0-2,4"` (cites cells 1-3 and cell 5)
29 | 4. **Open-ended Range**: From specified index to the end of notebook, such as `"3-"` (cites from cell 4 to the last cell)
30 | - `--notebook_path`: Name of the notebook to cite cells from, default ("") to current activated notebook
31 |
32 | #### Output Format
33 |
34 | ```
35 | USER Cite cells {cell_indices} from notebook {notebook_name}, here are the cells:
36 | =====Cell {cell_index} | type: {cell_type} | execution count: {execution_count}=====
37 | {cell_source}
38 | ...(other cells)
39 | =====End of Cited Cells=====
40 | USER's Instruction are follow: {prompt}
41 | ```
--------------------------------------------------------------------------------
/.github/workflows/fix-license-header.yml:
--------------------------------------------------------------------------------
1 | name: Fix License Headers
2 |
3 | on:
4 | pull_request_target:
5 |
6 | concurrency:
7 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
8 | cancel-in-progress: true
9 |
10 | jobs:
11 | header-license-fix:
12 | runs-on: ubuntu-latest
13 |
14 | permissions:
15 | contents: write
16 | pull-requests: write
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v5
21 | with:
22 | token: ${{ secrets.GITHUB_TOKEN }}
23 |
24 | - name: Checkout the branch from the PR that triggered the job
25 | run: gh pr checkout ${{ github.event.pull_request.number }}
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 |
29 | - name: Fix License Header
30 | # pin to include https://github.com/apache/skywalking-eyes/pull/168
31 | uses: apache/skywalking-eyes/header@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1
32 | with:
33 | mode: fix
34 |
35 | - name: List files changed
36 | id: files-changed
37 | shell: bash -l {0}
38 | run: |
39 | set -ex
40 | export CHANGES=$(git status --porcelain | tee /tmp/modified.log | wc -l)
41 | cat /tmp/modified.log
42 |
43 | echo "N_CHANGES=${CHANGES}" >> $GITHUB_OUTPUT
44 |
45 | git diff
46 |
47 | - name: Commit any changes
48 | if: steps.files-changed.outputs.N_CHANGES != '0'
49 | shell: bash -l {0}
50 | run: |
51 | git config user.name "github-actions[bot]"
52 | git config user.email "github-actions[bot]@users.noreply.github.com"
53 |
54 | git pull --no-tags
55 |
56 | git add *
57 | git commit -m "Automatic application of license header"
58 |
59 | git config push.default upstream
60 | git push
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | # Copyright (c) Datalayer, Inc. https://datalayer.io
6 | # Distributed under the terms of the MIT License.
7 |
8 | SHELL=/bin/bash
9 |
10 | .DEFAULT_GOAL := default
11 |
12 | CONDA_ACTIVATE=source $$(conda info --base)/etc/profile.d/conda.sh ; conda activate
13 | CONDA_DEACTIVATE=source $$(conda info --base)/etc/profile.d/conda.sh ; conda deactivate
14 | CONDA_REMOVE=source $$(conda info --base)/etc/profile.d/conda.sh ; conda remove -y --all -n
15 |
16 | ENV_NAME=datalayer
17 |
18 | .SILENT: init install
19 |
20 | .PHONY: build publish
21 |
22 | help: ## display this help
23 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
24 |
25 | default: help ## default target is help
26 |
27 | env-rm:
28 | -conda remove -y --all -n ${ENV_NAME}
29 |
30 | env:
31 | -conda env create -f environment.yml
32 | @echo
33 | @echo --------------------------------------------------
34 | @echo ✨ Datalayer environment is created.
35 | @echo --------------------------------------------------
36 | @echo
37 |
38 | clean: ## clear
39 | ($(CONDA_ACTIVATE) ${ENV_NAME}; \
40 | npm clear )
41 |
42 | install: ## install
43 | ($(CONDA_ACTIVATE) ${ENV_NAME}; \
44 | npm install )
45 |
46 | start: ## start
47 | ($(CONDA_ACTIVATE) ${ENV_NAME}; \
48 | npm start )
49 |
50 | build: ## build
51 | ($(CONDA_ACTIVATE) ${ENV_NAME}; \
52 | npm run build )
53 |
54 | publish: build ## publish
55 | ($(CONDA_ACTIVATE) ${ENV_NAME}; \
56 | aws s3 rm \
57 | s3://datalayer-jupyter-mcp-server/ \
58 | --recursive \
59 | --profile datalayer && \
60 | aws s3 cp \
61 | ./build \
62 | s3://datalayer-jupyter-mcp-server/ \
63 | --recursive \
64 | --profile datalayer && \
65 | aws cloudfront create-invalidation \
66 | --distribution-id EP7AV0D2EWHSX \
67 | --paths "/*" \
68 | --profile datalayer && \
69 | echo open ✨ https://jupyter-mcp-server.datalayer.tech )
70 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/_base.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Base classes and enums for MCP tools."""
6 |
7 | from abc import ABC, abstractmethod
8 | from enum import Enum
9 | from typing import Any, Optional
10 |
11 | from jupyter_server_client import JupyterServerClient
12 | from jupyter_kernel_client import KernelClient
13 |
14 |
15 | class ServerMode(str, Enum):
16 | """Enum to indicate which server mode the tool is running in."""
17 | MCP_SERVER = "mcp_server"
18 | JUPYTER_SERVER = "jupyter_server"
19 |
20 |
21 | class BaseTool(ABC):
22 | """Abstract base class for all MCP tools.
23 |
24 | Each tool must implement the execute method which handles both
25 | MCP_SERVER mode (using HTTP clients) and JUPYTER_SERVER mode
26 | (using direct API access to serverapp managers).
27 | """
28 |
29 | def __init__(self):
30 | """Initialize the tool."""
31 | pass
32 |
33 | @abstractmethod
34 | async def execute(
35 | self,
36 | mode: ServerMode,
37 | server_client: Optional[JupyterServerClient] = None,
38 | kernel_client: Optional[KernelClient] = None,
39 | contents_manager: Optional[Any] = None,
40 | kernel_manager: Optional[Any] = None,
41 | kernel_spec_manager: Optional[Any] = None,
42 | **kwargs
43 | ) -> Any:
44 | """Execute the tool logic.
45 |
46 | Args:
47 | mode: ServerMode indicating MCP_SERVER or JUPYTER_SERVER
48 | server_client: JupyterServerClient for HTTP access (MCP_SERVER mode)
49 | kernel_client: KernelClient for kernel HTTP access (MCP_SERVER mode)
50 | contents_manager: Direct access to contents manager (JUPYTER_SERVER mode)
51 | kernel_manager: Direct access to kernel manager (JUPYTER_SERVER mode)
52 | kernel_spec_manager: Direct access to kernel spec manager (JUPYTER_SERVER mode)
53 | **kwargs: Tool-specific parameters
54 |
55 | Returns:
56 | Tool execution result (type varies by tool)
57 | """
58 | pass
59 |
--------------------------------------------------------------------------------
/tests/test_prompts.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Test for MCP Prompts Feature
7 | """
8 |
9 | import os
10 |
11 | import pytest
12 |
13 | from .test_common import MCPClient, timeout_wrapper
14 |
15 | # Now, prompt feature is only available in MCP_SERVER mode.
16 | pytestmark = pytest.mark.skipif(
17 | not os.environ.get("TEST_MCP_SERVER", "false").lower() == "true",
18 | reason="Prompt feature is only available in MCP_SERVER mode now."
19 | )
20 |
21 |
22 | @pytest.mark.asyncio
23 | @timeout_wrapper(60)
24 | async def test_jupyter_cite(mcp_client_parametrized: MCPClient):
25 | """Test jupyter cite prompt feature"""
26 | async with mcp_client_parametrized:
27 | await mcp_client_parametrized.use_notebook("new", "new.ipynb")
28 | await mcp_client_parametrized.use_notebook("notebook", "notebook.ipynb")
29 | # Test prompt injection
30 | response = await mcp_client_parametrized.jupyter_cite(prompt="test prompt", cell_indices="0")
31 | assert "# Matplotlib Examples" in response[0], "Cell 0 should contain Matplotlib Examples"
32 | assert "test prompt" in response[0], "Prompt should be injected"
33 | # Test mixed cell_indices
34 | response = await mcp_client_parametrized.jupyter_cite(prompt="", cell_indices="0-2,4")
35 | assert "USER Cite cells [0, 1, 2, 4]" in response[0], "Cell indices should be [0, 1, 2, 4]"
36 | assert "## 1. Import Required Libraries" in response[0], "Cell 1 should contain Import Required Libraries"
37 | assert "%matplotlib inline" in response[0], "Cell 2 should contain %matplotlib inline"
38 | assert "## 2. Basic Line Plot" not in response[0], "Cell 3 should not be cited"
39 | assert "y = np.sin(x)" in response[0], "Cell 4 should contain y = np.sin(x)"
40 | # Test cite other notebook
41 | response = await mcp_client_parametrized.jupyter_cite(prompt="", cell_indices="0", notebook_name="new")
42 | assert "from notebook new" in response[0], "should cite new notebook"
43 | assert "# A New Notebook" in response[0], "Cell 0 of new notebook should contain A New Notebook"
44 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageProducts.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | import React from 'react';
8 | import clsx from 'clsx';
9 | import styles from './HomepageProducts.module.css';
10 |
11 | const ProductList = [
12 | /*
13 | {
14 | title: 'Jupyter MCP Server',
15 | Svg: require('../../static/img/product_1.svg').default,
16 | description: (
17 | <>
18 | Get started by creating a Jupyter platform in the cloud with Jupyter MCP Server. You will get Jupyter on Kubernetes with a cloud database and storage bucket to persist your notebooks and datasets.
19 | >
20 | ),
21 | },
22 | {
23 | title: 'Jupyter',
24 | Svg: require('../../static/img/product_2.svg').default,
25 | description: (
26 | <>
27 | If you need more batteries for Jupyter, have a look to our Jupyter components. The components allow you to get the best of Jupyter notebooks, with features like authentication, authorization, React.js user interface, server and kernel instant start, administration...
28 | >
29 | ),
30 | },
31 | {
32 | title: 'Sharebook',
33 | Svg: require('../../static/img/product_3.svg').default,
34 | description: (
35 | <>
36 | For a truly collaborative and accessible notebook, try Sharebook, a better better literate notebook, with built-in collaboration, accessibility...
37 | >
38 | ),
39 | },
40 | */
41 | ];
42 |
43 | function Product({Svg, title, description}) {
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
{title}
51 |
{description}
52 |
53 |
54 | );
55 | }
56 |
57 | export default function HomepageProducts() {
58 | return (
59 |
60 |
61 |
62 | {ProductList.map((props, idx) => (
63 |
64 | ))}
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/prompt/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # 📋 Jupyter MCP Server Prompt Templates
8 |
9 | Welcome to the Jupyter MCP Server Prompt Templates repository! This directory contains curated, community-driven prompt templates designed to help AI agents and users make the most of Jupyter MCP Server across different scenarios and use cases.
10 |
11 | ## 💡 How to Use Prompt Templates
12 |
13 | Templates are organized by use case, you can choose any of them.
14 |
15 | > [!TIP]
16 | >
17 | > Start with the [`general/`](general/) template if you're new to Jupyter MCP Server. It provides foundational guidance applicable to most use cases.
18 |
19 | ### Example Usage
20 |
21 | ```
22 | 1. Go to prompt/general/
23 | 2. Read the README.md to understand the template's purpose
24 | 3. Copy the content from AGENT.md
25 | 4. Paste it as your system prompt(e.g. `CLAUDE.md` in Claude Code)
26 | 5. Start your session with enhanced context!
27 | ```
28 |
29 | ## 🤝 Contributing Your Own Templates
30 |
31 | We love community contributions! If you've developed a great prompt template that works well with Jupyter MCP, here's how to share it:
32 |
33 | ### Step 1: Create a Template Directory
34 |
35 | Clone the repository and create a new folder with a clear, concise name representing the use case:
36 |
37 | ```bash
38 | git clone https://github.com/datalayer/jupyter-mcp-server.git
39 | cd jupyter-mcp-server/prompt
40 | mkdir your-use-case/
41 | ```
42 |
43 | ### Step 2: Create a README.md
44 |
45 | create a `README.md` file in the new folder and it should include the brief description of what this template is for.
46 |
47 | ### Step 3: Create the AGENT.md
48 |
49 | Create an `AGENT.md` file containing the actual system prompt for AI agents.
50 |
51 | ### Step 4: Add Resources (Optional)
52 |
53 | You can include additional files to enhance your template
54 |
55 | ### Step 5: Submit a Pull Request
56 |
57 | Create a pull request to let us know your Awesome Prompt Template!
58 |
59 | ## 🙏 Thank You
60 |
61 | Thank you for using and contributing to Jupyter MCP Server Prompt Templates! Your participation helps build a stronger community and makes AI-powered notebook workflows more accessible to everyone.
62 |
--------------------------------------------------------------------------------
/docs/docs/getting_started/index.mdx:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | Jupyter MCP Server can be started in various configurations depending on your needs. It can be running inside the Jupyter Server **as a Jupyter Server Extension**, or as a **Standalone Server** connecting to a **local or remote Jupyter server** or to [**Datalayer**](https://datalayer.ai) hosted Notebooks.
4 |
5 | Navigate to the relevant section based on your needs:
6 | - ***Jupyter Notebooks***: If you want to interact with notebooks in JupyterLab/JupyterHub.
7 | - ***Datalayer Notebooks***: If you want to interact with notebooks hosted on [Datalayer](https://datalayer.ai).
8 | - ***STDIO Transport***: If you want to set up the MCP Server using standard input/output (STDIO) transport.
9 | - ***Streamable HTTP Transport***: If you want to set up the MCP Server using Streamable HTTP transport.
10 | - ***As a Standalone Server***: If you want to set up the MCP Server as a Standalone Server.
11 | - ***As a Jupyter Server Extension***: If you want to set up the MCP Server as a Jupyter Server Extension. This has for advantage to avoid running 2 separate servers (Jupyter server + MCP server) but only supports Streamable HTTP transport.
12 |
13 | You can find below diagrams illustrating the different configurations.
14 |
15 | ## As a Standalone Server
16 |
17 | The following diagram illustrates how **Jupyter MCP Server** connects to a **Jupyter server** or **Datalayer** and communicates with an MCP client.
18 |
19 |
24 |
25 | ## As a Jupyter Server Extension
26 |
27 | The following diagram illustrates how **Jupyter MCP Server** runs as an extension inside a **Jupyter server** and communicates with an MCP client.
28 | In this configuration, you don't need to run a separate MCP server. It will start automatically when you start your Jupyter server.
29 | Note that only **Streamable HTTP** transport is supported in this configuration.
30 |
31 |
36 |
--------------------------------------------------------------------------------
/prompt/general/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ## 📝 Overview
8 |
9 | This is the **general-purpose** prompt template for Jupyter MCP Server. It provides foundational guidance and best practices for using Jupyter MCP Server across a wide variety of use cases. **If you're new to Jupyter MCP, start here!**
10 |
11 | ## 💡 Core Philosophy: Explorer, Not Builder
12 |
13 | The agent's core concept is to be an **Explorer, not a Builder**. It treats user requests as scientific inquiries rather than simple engineering tasks.
14 |
15 | To achieve this, the agent follows the **Introspective Exploration Loop**:
16 |
17 | 1. **Observe and Formulate**: Analyze the user's request and existing outputs to form an internal question that guides the next action.
18 | 2. **Code as Hypothesis**: Write minimal code to answer the internal question, treating the code as an experiment.
19 | 3. **Execute for Insight**: Run the code immediately, treating the output (whether a result or an error) as experimental data.
20 | 4. **Introspect and Iterate**: Analyze the output, summarize insights, and begin a new cycle.
21 |
22 | ## 🚀 User Guide: How to Customize the Agent
23 |
24 | You can "fine-tune" the agent for your project's specific needs by modifying the `Custom Context` within `AGENT.md`.
25 |
26 | Open `AGENT.md` and find the `# Context` section:
27 |
28 | ```markdown
29 | # Context
30 |
31 | {{Add your custom context here, like your package installation, preferred code style, etc.}}
32 | ```
33 |
34 | Replace the `{{...}}` placeholder with your project-specific rules.
35 |
36 | #### Example:
37 |
38 | To make the agent prefer the `Polars` library and adhere to the `black` code style, you would modify it like this:
39 |
40 | ```markdown
41 | # Context
42 |
43 | - **Library Preference**: Prioritize using the `Polars` library for data manipulation instead of `Pandas`.
44 | - **Code Style**: All Python code should be formatted according to the `black` code style.
45 | - **Project Background**: This project aims to analyze user behavior data, and the key data file is `user_behavior.csv`.
46 | ```
47 |
48 | ---
49 |
50 | - **Version**: 1.0.0
51 | - **Author**: Jupyter MCP Server Community
52 | - **Last Update**: 2025-11-01
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info/
2 | .ipynb_checkpoints
3 |
4 | # Created by https://www.gitignore.io/api/python
5 | # Edit at https://www.gitignore.io/?templates=python
6 |
7 | ### Python ###
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | dist/
20 | downloads/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | pip-wheel-metadata/
28 | share/python-wheels/
29 | .installed.cfg
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Environment variables:
59 | .env
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # pyenv
71 | .python-version
72 |
73 | # celery beat schedule file
74 | celerybeat-schedule
75 |
76 | # SageMath parsed files
77 | *.sage.py
78 |
79 | # Spyder project settings
80 | .spyderproject
81 | .spyproject
82 |
83 | # Rope project settings
84 | .ropeproject
85 |
86 | # Mr Developer
87 | .mr.developer.cfg
88 | .project
89 | .pydevproject
90 |
91 | # mkdocs documentation
92 | /site
93 |
94 | # mypy
95 | .mypy_cache/
96 | .dmypy.json
97 | dmypy.json
98 |
99 | # ruff
100 | .ruff_cache
101 |
102 | # Pyre type checker
103 | .pyre/
104 |
105 | # End of https://www.gitignore.io/api/python
106 |
107 | # OSX files
108 | .DS_Store
109 |
110 | # Include
111 | !**/.*ignore
112 | !**/.*rc
113 | !**/.*rc.js
114 | !**/.*rc.json
115 | !**/.*rc.yml
116 | !**/.*config
117 | !*.*rc.json
118 | !.github
119 | !.devcontainer
120 |
121 | untracked_notebooks/*
122 | .jupyter_ystore
123 | .jupyter_ystore.db
124 | docs/.yarn/*
125 |
126 | uv.lock
127 | *-lock.json
128 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Tools package for Jupyter MCP Server.
6 |
7 | Each tool is implemented as a separate class with an execute method
8 | that can operate in either MCP_SERVER or JUPYTER_SERVER mode.
9 | """
10 |
11 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode
12 |
13 | # Import tool implementations - Notebook Management
14 | from jupyter_mcp_server.tools.list_notebooks_tool import ListNotebooksTool
15 | from jupyter_mcp_server.tools.restart_notebook_tool import RestartNotebookTool
16 | from jupyter_mcp_server.tools.unuse_notebook_tool import UnuseNotebookTool
17 | from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool
18 |
19 | # Import tool implementations - Cell Reading
20 | from jupyter_mcp_server.tools.read_notebook_tool import ReadNotebookTool
21 | from jupyter_mcp_server.tools.read_cell_tool import ReadCellTool
22 |
23 | # Import tool implementations - Cell Writing
24 | from jupyter_mcp_server.tools.insert_cell_tool import InsertCellTool
25 | from jupyter_mcp_server.tools.overwrite_cell_source_tool import OverwriteCellSourceTool
26 | from jupyter_mcp_server.tools.delete_cell_tool import DeleteCellTool
27 |
28 | # Import tool implementations - Cell Execution
29 | from jupyter_mcp_server.tools.execute_cell_tool import ExecuteCellTool
30 |
31 | # Import tool implementations - Other Tools
32 | from jupyter_mcp_server.tools.execute_code_tool import ExecuteCodeTool
33 | from jupyter_mcp_server.tools.list_files_tool import ListFilesTool
34 | from jupyter_mcp_server.tools.list_kernels_tool import ListKernelsTool
35 |
36 | # Import MCP prompt
37 | from jupyter_mcp_server.tools.jupyter_cite_prompt import JupyterCitePrompt
38 |
39 | __all__ = [
40 | "BaseTool",
41 | "ServerMode",
42 | # Notebook Management
43 | "ListNotebooksTool",
44 | "RestartNotebookTool",
45 | "UnuseNotebookTool",
46 | "UseNotebookTool",
47 | # Cell Reading
48 | "ReadNotebookTool",
49 | "ReadCellTool",
50 | # Cell Writing
51 | "InsertCellTool",
52 | "OverwriteCellSourceTool",
53 | "DeleteCellTool",
54 | # Cell Execution
55 | "ExecuteCellTool",
56 | # Other Tools
57 | "ExecuteCodeTool",
58 | "ListFilesTool",
59 | "ListKernelsTool",
60 | # MCP Prompt
61 | "JupyterCitePrompt",
62 | ]
63 |
64 |
65 |
--------------------------------------------------------------------------------
/docs/docs/jupyter/streamable-http/jupyter-extension/index.mdx:
--------------------------------------------------------------------------------
1 | # As a Jupyter Server Extension
2 |
3 | ## 1. Start JupyterLab and the MCP Server
4 |
5 | ### Environment setup
6 |
7 | Make sure you have the following packages installed in your environment. The collaboration package is needed as the modifications made on the notebook can be seen thanks to [Jupyter Real Time Collaboration](https://jupyterlab.readthedocs.io/en/stable/user/rtc.html).
8 |
9 | ```bash
10 | pip install "jupyter-mcp-server>=0.15.0" "jupyterlab==4.4.1" "jupyter-collaboration==4.0.2" "ipykernel"
11 | pip uninstall -y pycrdt datalayer_pycrdt
12 | pip install datalayer_pycrdt==0.12.17
13 | ```
14 |
15 | :::tip
16 | To confirm your environment is correctly configured:
17 | 1. Open a notebook in JupyterLab
18 | 2. Type some content in any cell (code or markdown)
19 | 3. Observe the tab indicator: you should see an "×" appear next to the notebook name, indicating unsaved changes
20 | 4. Wait a few seconds—the "×" should automatically change to a "●" without manually saving
21 |
22 | This automatic saving behavior confirms that the real-time collaboration features are working properly, which is essential for MCP server integration.
23 | :::
24 |
25 | ### Start JupyterLab with MCP Extension
26 |
27 | Start JupyterLab with the MCP server extension:
28 |
29 | ```bash
30 | jupyter lab --port 4040
31 | ```
32 |
33 | This starts JupyterLab at [http://127.0.0.1:4040](http://127.0.0.1:4040) with the MCP server integrated.
34 |
35 | For complete configuration options, see the [server configuration guide](/configuration).
36 |
37 | ## 2. Configure your MCP Client
38 |
39 | Use the following configuration to connect to the integrated MCP server:
40 |
41 | ```json
42 | {
43 | "mcpServers": {
44 | "jupyter": {
45 | "command": "npx",
46 | "args": ["mcp-remote", "http://127.0.0.1:4040/mcp"]
47 | }
48 | }
49 | }
50 | ```
51 |
52 | ## Troubleshooting
53 |
54 | ### Common Issues
55 |
56 | **Extension not loading:**
57 | - Verify `jupyter-mcp-server` is installed: `pip list | grep jupyter-mcp-server`
58 | - Check JupyterLab logs for extension errors
59 |
60 | **MCP endpoint not accessible:**
61 | - Verify server is running at: `curl http://localhost:4040/mcp`
62 | - Check that port 4040 is not blocked
63 |
64 | For detailed configuration and troubleshooting, see the [configuration guide](/configuration).
65 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/server_modes.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Utility functions for detecting and handling server mode."""
6 |
7 | from typing import Tuple, Optional, Any
8 | from jupyter_server_client import JupyterServerClient
9 | from jupyter_mcp_server.config import get_config
10 |
11 |
12 | def get_server_mode_and_clients() -> Tuple[str, Optional[JupyterServerClient], Optional[Any], Optional[Any], Optional[Any]]:
13 | """Determine server mode and get appropriate clients/managers.
14 |
15 | Returns:
16 | Tuple of (mode, server_client, contents_manager, kernel_manager, kernel_spec_manager)
17 | - mode: "local" if using local API, "http" if using HTTP clients
18 | - server_client: JupyterServerClient or None
19 | - contents_manager: Local contents manager or None
20 | - kernel_manager: Local kernel manager or None
21 | - kernel_spec_manager: Local kernel spec manager or None
22 | """
23 | config = get_config()
24 |
25 | # Check if we should use local API
26 | try:
27 | from jupyter_mcp_server.jupyter_extension.context import get_server_context
28 | context = get_server_context()
29 |
30 | if context.is_local_document() and context.get_contents_manager() is not None:
31 | # JUPYTER_SERVER mode with local API access
32 | return (
33 | "local",
34 | None,
35 | context.get_contents_manager(),
36 | context.get_kernel_manager(),
37 | context.get_kernel_spec_manager()
38 | )
39 | except (ImportError, Exception):
40 | # Context not available or error, fall through to HTTP mode
41 | pass
42 |
43 | # MCP_SERVER mode with HTTP clients
44 | server_client = JupyterServerClient(
45 | base_url=config.runtime_url,
46 | token=config.runtime_token
47 | )
48 |
49 | return ("http", server_client, None, None, None)
50 |
51 |
52 | def is_local_mode() -> bool:
53 | """Check if running in local API mode.
54 |
55 | Returns:
56 | True if using local serverapp API, False if using HTTP clients
57 | """
58 | try:
59 | from jupyter_mcp_server.jupyter_extension.context import get_server_context
60 | context = get_server_context()
61 | return context.is_local_document() and context.get_contents_manager() is not None
62 | except (ImportError, Exception):
63 | return False
64 |
--------------------------------------------------------------------------------
/docs/docs/resources/index.mdx:
--------------------------------------------------------------------------------
1 | # Resources
2 |
3 | ## Articles & Blog Posts
4 |
5 | - [HuggingFace Blog - How to Install and Use Jupyter MCP Server](https://huggingface.co/blog/lynn-mikami/jupyter-mcp-server)
6 | - [Analytics Vidhya - How to Use Jupyter MCP Server?](https://www.analyticsvidhya.com/blog/2025/05/jupyter-mcp-server/)
7 | - [Medium AI Simplified in Plain English - How to Use Jupyter MCP Server?](https://medium.com/ai-simplified-in-plain-english/how-to-use-jupyter-mcp-server-87f68fea7471)
8 | - [Medium Jupyter AI Agents - Jupyter MCP Server: How to Setup via Claude Desktop](https://jupyter-ai-agents.datalayer.blog/mcp-server-for-jupyter-heres-your-guide-2025-0b29d975b4e1)
9 | - [Medium Data Science in Your Pocket - Best MCP Servers for Data Scientists](https://medium.com/data-science-in-your-pocket/best-mcp-servers-for-data-scientists-ee4fa6caf066)
10 | - [Medium Coding Nexus - 6 Open Source MCP Servers Every Dev Should Try](https://medium.com/coding-nexus/6-open-source-mcp-servers-every-dev-should-try-b3cc6cf6a714)
11 | - [Medium Joe Njenga - 8 Best MCP Servers & Tools Every Python Developer Should Try](https://medium.com/@joe.njenga/8-best-mcp-servers-tools-every-python-developer-should-try-3e69f435e99e)
12 | - [Medium Sreekar Kashyap - MCP Servers + Ollama](https://medium.com/@sreekarkashyap7/mcp-servers-ollama-fad991461e88)
13 | - [Medium Wenmin Wu - Agentic DS Workflow with Cursor and MCP Servers](https://medium.com/@wenmin_wu/agentic-ds-workflow-with-cursor-and-mcp-servers-2d90a102cf31)
14 |
15 | ## Videos
16 |
17 | - [Data Science in your pocket - Jupyter MCP : AI for Jupyter Notebooks](https://www.youtube.com/watch?v=qkoEsqiWDOU)
18 | - [Datalayer - How to Set Up the Jupyter MCP Server (via Claude Desktop)](https://www.youtube.com/watch?v=nPllCQxtaxQ)
19 |
20 | ## MCP Directories
21 |
22 | - [Model Context Protocol Servers](https://github.com/modelcontextprotocol/servers)
23 | - [Awesome MCP Servers](https://github.com/punkpeye/awesome-mcp-servers)
24 |
25 | ## MCP Registries
26 |
27 | - [MCP.so](https://mcp.so/server/Integrating-the-Jupyter-server-with-claude-desktop-uisng-the-powerful-model-context-protocol/harshitha-8)
28 | - [MCP Market](https://mcpmarket.com/server/jupyter)
29 | - [MCP Servers Finder](https://www.mcpserverfinder.com/servers/ihrpr/mcp-server-jupyter)
30 | - [Pulse MCP](https://www.pulsemcp.com/servers/datalayer-jupyter)
31 | - [Playbooks](https://playbooks.com/mcp/datalayer-jupyter)
32 | - [Know That AI](https://knowthat.ai/agents/jupyter-server)
33 |
34 |
37 |
--------------------------------------------------------------------------------
/docs/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overview
3 | sidebar_position: 1
4 | hide_table_of_contents: false
5 | slug: /
6 | ---
7 |
8 | # Overview
9 |
10 | :::info
11 |
12 | **🚨 NEW IN 0.14.0:** Multi-notebook support!
13 | You can now seamlessly switch between multiple notebooks in a single session.
14 | [Read more in the release notes.](https://jupyter-mcp-server.datalayer.tech/releases)
15 |
16 | :::
17 |
18 | **Jupyter MCP Server** is a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server implementation that enables **real-time** interaction with 📓 Jupyter Notebooks, allowing AI to edit, document and execute code for data analysis, visualization etc.
19 |
20 | Compatible with any Jupyter deployment (local, JupyterHub, ...) and with [Datalayer](https://datalayer.ai) hosted Notebooks. [Open an issue](https://github.com/datalayer/jupyter-mcp-server/issues) to discuss adding your solution as provider.
21 |
22 | Key features include:
23 |
24 | - ⚡ **Real-time control:** Instantly view notebook changes as they happen.
25 | - 🔁 **Smart execution:** Automatically adjusts when a cell run fails thanks to cell output feedback.
26 | - 🧠 **Context-aware:** Understands the entire notebook context for more relevant interactions.
27 | - 📊 **Multimodal support:** Support different output types, including images, plots, and text.
28 | - 📁 **Multi-notebook support:** Seamlessly switch between multiple notebooks.
29 | - 🎛️ **JupyterLab integration:** Enhanced UI integration like automatic notebook opening.
30 | - 🤝 **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.
31 |
32 | To use Jupyter MCP Server, you first need to decide which setup fits your needs:
33 | - ***Editor***: Do you want to interact with notebooks in Jupyter or with Datalayer hosted Notebooks?
34 | - ***MCP Transport***: Do you want to set up the MCP Server using standard input/output (STDIO) transport or Streamable HTTP transport?
35 | - ***MCP Server Location***: Do you want to set up the MCP Server as a Standalone Server or as a Jupyter Server Extension?
36 |
37 | Navigate to the relevant section in the [Getting Started](./getting_started) page to get started based on your needs.
38 |
39 | Looking for blog posts, videos or other resources related to Jupyter MCP Server?
40 | 👉 Check out the [Resources](./resources) section.
41 |
42 | 🧰 Dive into the [Tools section](./tools) to understand the tools powering the server.
43 |
44 | 
45 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/list_notebooks_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """List notebooks tool implementation."""
6 |
7 | from typing import Any, Optional
8 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode
9 | from jupyter_mcp_server.notebook_manager import NotebookManager
10 | from jupyter_mcp_server.utils import format_TSV
11 |
12 |
13 | class ListNotebooksTool(BaseTool):
14 | """Tool to list all managed notebooks (that have been used via use_notebook)."""
15 |
16 | async def execute(
17 | self,
18 | mode: ServerMode,
19 | server_client: Optional[Any] = None,
20 | contents_manager: Optional[Any] = None,
21 | kernel_manager: Optional[Any] = None,
22 | kernel_spec_manager: Optional[Any] = None,
23 | notebook_manager: Optional[NotebookManager] = None,
24 | **kwargs
25 | ) -> str:
26 | """Execute the list_notebooks tool.
27 |
28 | This tool lists all notebooks that have been managed through the use_notebook tool.
29 | It does NOT perform recursive filesystem scanning.
30 |
31 | Args:
32 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
33 | notebook_manager: Notebook manager instance
34 | **kwargs: Additional parameters (unused)
35 |
36 | Returns:
37 | TSV formatted table with managed notebook information
38 | """
39 | if notebook_manager is None:
40 | return "No notebook manager available."
41 |
42 | # Get all managed notebooks
43 | managed_notebooks = notebook_manager.list_all_notebooks()
44 |
45 | if not managed_notebooks:
46 | return "No managed notebooks. Use the use_notebook tool to manage notebooks first."
47 |
48 | # Create TSV formatted output
49 | headers = ["Name", "Path", "Kernel_ID", "Kernel_Status", "Activate"]
50 | rows = []
51 |
52 | # Sort by name for consistent output
53 | for name in sorted(managed_notebooks.keys()):
54 | info = managed_notebooks[name]
55 | activate_marker = "✓" if info.get("is_current") else ""
56 | # Get kernel_id from notebook_manager
57 | kernel_id = notebook_manager.get_kernel_id(name) or "-"
58 | rows.append([
59 | name,
60 | info.get("path", "-"),
61 | kernel_id,
62 | info.get("kernel_status", "unknown"),
63 | activate_marker
64 | ])
65 |
66 | return format_TSV(headers, rows)
67 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 |
8 | defaults:
9 | run:
10 | shell: bash -eux {0}
11 |
12 | jobs:
13 | build:
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | os: [ubuntu-latest, macos-latest, windows-latest]
19 | python-version: ["3.10", "3.13"]
20 |
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v5
24 |
25 | - name: Set up Python ${{ matrix.python-version }}
26 | uses: actions/setup-python@v6
27 | with:
28 | python-version: ${{ matrix.python-version }}
29 |
30 | - name: Base Setup
31 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
32 |
33 | - name: Install the extension
34 | run: |
35 | python -m pip install ".[test]"
36 |
37 | - name: Build the extension
38 | run: |
39 | pip install build
40 | python -m build --sdist
41 | cp dist/*.tar.gz jupyter_mcp_server.tar.gz
42 | pip uninstall -y "jupyter_mcp_server"
43 | rm -rf "jupyter_mcp_server"
44 |
45 | - uses: actions/upload-artifact@v5
46 | if: startsWith(matrix.os, 'ubuntu')
47 | with:
48 | name: jupyter_mcp_server-sdist-${{ matrix.python-version }}
49 | path: jupyter_mcp_server.tar.gz
50 |
51 | # check_links:
52 | # runs-on: ubuntu-latest
53 | # steps:
54 | # - uses: actions/checkout@v5
55 | # - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
56 | # - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
57 |
58 | test_lint:
59 | runs-on: ubuntu-latest
60 | steps:
61 | - uses: actions/checkout@v5
62 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
63 | - name: Run Linters
64 | run: |
65 | bash ./.github/workflows/lint.sh
66 |
67 | test_sdist:
68 | needs: build
69 | runs-on: ubuntu-latest
70 | strategy:
71 | matrix:
72 | python-version: ["3.13"]
73 |
74 | steps:
75 | - name: Checkout
76 | uses: actions/checkout@v5
77 | - name: Install Python
78 | uses: actions/setup-python@v6
79 | with:
80 | python-version: ${{ matrix.python-version }}
81 | architecture: "x64"
82 | - uses: actions/download-artifact@v6
83 | with:
84 | name: jupyter_mcp_server-sdist-${{ matrix.python-version }}
85 | - name: Install and Test
86 | run: |
87 | pip install jupyter_mcp_server.tar.gz
88 | pip list 2>&1 | grep -ie "jupyter_mcp_server"
89 | python -c "import jupyter_mcp_server"
90 |
--------------------------------------------------------------------------------
/docs/docs/releases/index.mdx:
--------------------------------------------------------------------------------
1 | # Releases
2 |
3 | ## Latest Release
4 |
5 | See the latest release notes on the [GitHub Releases page](https://github.com/datalayer/jupyter-mcp-server/releases).
6 |
7 | ## Older Releases
8 |
9 | ### 0.16.x - 13 Oct 2025
10 |
11 | - [Merge the three execute tools into a single unified tool](https://github.com/datalayer/jupyter-mcp-server/pull/111)
12 | - [Docs: update readme and contributing](https://github.com/datalayer/jupyter-mcp-server/pull/112)
13 | - CI: build Auto Releases CI/CD: [#114](https://github.com/datalayer/jupyter-mcp-server/pull/114), [#115](https://github.com/datalayer/jupyter-mcp-server/pull/115), [#119](https://github.com/datalayer/jupyter-mcp-server/pull/119), [#120](https://github.com/datalayer/jupyter-mcp-server/pull/120)
14 | - [fix negative index](https://github.com/datalayer/jupyter-mcp-server/pull/116)
15 | - [fix ressources and prompts list](https://github.com/datalayer/jupyter-mcp-server/pull/117)
16 | - [Refactor: separate and simplify the server.py](https://github.com/datalayer/jupyter-mcp-server/pull/123)
17 | - [Feat/add JUPYTER_URL and JUPYTER_TOKEN](https://github.com/datalayer/jupyter-mcp-server/pull/125)
18 |
19 | ### 0.15.x - 08 Oct 2025
20 |
21 | - [Run as Jupyter Server Extension + Tool registry + Use tool](https://github.com/datalayer/jupyter-mcp-server/pull/95)
22 | - [simplify tool implementations](https://github.com/datalayer/jupyter-mcp-server/pull/101)
23 | - [add uvx as alternative MCP server startup method](https://github.com/datalayer/jupyter-mcp-server/pull/101)
24 | - [document as a Jupyter Extension](https://github.com/datalayer/jupyter-mcp-server/pull/101)
25 | - Fix Minor Bugs: [#108](https://github.com/datalayer/jupyter-mcp-server/pull/108),[#110](https://github.com/datalayer/jupyter-mcp-server/pull/110)
26 |
27 | ### 0.14.0 - 03 Oct 2025
28 |
29 | - [Additional Tools & Bug fixes](https://github.com/datalayer/jupyter-mcp-server/pull/93).
30 | - [Execute IPython](https://github.com/datalayer/jupyter-mcp-server/pull/90).
31 | - [Multi notebook management](https://github.com/datalayer/jupyter-mcp-server/pull/88).
32 |
33 | ### 0.13.0 - 25 Sep 2025
34 |
35 | - [Add multimodal output support for Jupyter cell execution](https://github.com/datalayer/jupyter-mcp-server/pull/75).
36 | - [Unify cell insertion functionality](https://github.com/datalayer/jupyter-mcp-server/pull/73).
37 |
38 | ### 0.11.0 - 01 Aug 2025
39 |
40 | - [Rename room to document](https://github.com/datalayer/jupyter-mcp-server/pull/35).
41 |
42 | ### 0.10.2 - 17 Jul 2025
43 |
44 | - [Tools docstring improvements](https://github.com/datalayer/jupyter-mcp-server/pull/30).
45 |
46 | ### 0.10.1 - 11 Jul 2025
47 |
48 | - [CORS Support](https://github.com/datalayer/jupyter-mcp-server/pull/29).
49 |
50 | ### 0.10.0 - 07 Jul 2025
51 |
52 | - More [fixes](https://github.com/datalayer/jupyter-mcp-server/pull/28) issues for nbclient stop.
53 |
54 | ### 0.9.0 - 02 Jul 2025
55 |
56 | - Fix issues with `nbmodel` stops.
57 |
58 | ### 0.6.0 - 01 Jul 2025
59 |
60 | - Configuration change, see details on the [clients page](/clients) and [server configuration](/configuration).
61 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/read_notebook_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """List cells tool implementation."""
6 |
7 | from typing import Any, Optional, Literal
8 | from jupyter_server_client import JupyterServerClient
9 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode
10 | from jupyter_mcp_server.notebook_manager import NotebookManager
11 | from jupyter_mcp_server.models import Notebook
12 |
13 |
14 | class ReadNotebookTool(BaseTool):
15 | """Tool to read a notebook and return index, source content, type, execution count of each cell."""
16 |
17 | async def execute(
18 | self,
19 | mode: ServerMode,
20 | server_client: Optional[JupyterServerClient] = None,
21 | contents_manager: Optional[Any] = None,
22 | notebook_manager: Optional[NotebookManager] = None,
23 | notebook_name: str = None,
24 | response_format: Literal["brief", "detailed"] = "brief",
25 | start_index: int = 0,
26 | limit: int = 20,
27 | **kwargs
28 | ) -> str:
29 | """Execute the read_notebook tool.
30 |
31 | Args:
32 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
33 | contents_manager: Direct API access for JUPYTER_SERVER mode
34 | notebook_manager: Notebook manager instance
35 | notebook_name: Notebook identifier to read
36 | response_format: Response format (brief or detailed)
37 | start_index: Starting index for pagination (0-based)
38 | limit: Maximum number of items to return (0 means no limit)
39 | **kwargs: Additional parameters
40 |
41 | Returns:
42 | Formatted table with cell information
43 | """
44 | if notebook_name not in notebook_manager:
45 | return f"Notebook '{notebook_name}' is not connected. All currently connected notebooks: {list(notebook_manager.list_all_notebooks().keys())}"
46 |
47 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
48 | # Local mode: read notebook directly from file system
49 | notebook_path = notebook_manager.get_notebook_path(notebook_name)
50 |
51 | model = await contents_manager.get(notebook_path, content=True, type='notebook')
52 | if 'content' not in model:
53 | raise ValueError(f"Could not read notebook content from {notebook_path}")
54 | notebook = Notebook(**model['content'])
55 | elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
56 | # Remote mode: use WebSocket connection to Y.js document
57 | async with notebook_manager.get_notebook_connection(notebook_name) as notebook_content:
58 | notebook = Notebook(**notebook_content.as_dict())
59 | else:
60 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}")
61 |
62 | if start_index >= len(notebook):
63 | return f"Start index {start_index} is out of range. Notebook has {len(notebook)} cells."
64 |
65 | info_list = [f'Notebook {notebook_name} has {len(notebook)} cells.\n']
66 | info_list.append(notebook.format_output(response_format=response_format, start_index=start_index, limit=limit))
67 |
68 | return "\n".join(info_list)
69 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/read_cell_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Read cell tool implementation."""
6 |
7 | from typing import Any, Optional
8 | from jupyter_server_client import JupyterServerClient
9 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode
10 | from jupyter_mcp_server.notebook_manager import NotebookManager
11 | from jupyter_mcp_server.models import Notebook
12 | from jupyter_mcp_server.config import get_config
13 | from mcp.types import ImageContent
14 |
15 |
16 | class ReadCellTool(BaseTool):
17 | """Tool to read a specific cell from a notebook."""
18 |
19 | async def execute(
20 | self,
21 | mode: ServerMode,
22 | server_client: Optional[JupyterServerClient] = None,
23 | kernel_client: Optional[Any] = None,
24 | contents_manager: Optional[Any] = None,
25 | kernel_manager: Optional[Any] = None,
26 | kernel_spec_manager: Optional[Any] = None,
27 | notebook_manager: Optional[NotebookManager] = None,
28 | # Tool-specific parameters
29 | cell_index: int = None,
30 | include_outputs: bool = True,
31 | **kwargs
32 | ) -> list[str | ImageContent]:
33 | """Execute the read_cell tool.
34 |
35 | Args:
36 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
37 | contents_manager: Direct API access for JUPYTER_SERVER mode
38 | notebook_manager: Notebook manager instance
39 | cell_index: Index of the cell to read (0-based)
40 | include_outputs: Include outputs in the response (only for code cells)
41 | **kwargs: Additional parameters
42 |
43 | Returns:
44 | Cell information dictionary
45 | """
46 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None:
47 | # Local mode: read notebook directly from file system
48 | notebook_path = notebook_manager.get_current_notebook_path()
49 |
50 | model = await contents_manager.get(notebook_path, content=True, type='notebook')
51 | if 'content' not in model:
52 | raise ValueError(f"Could not read notebook content from {notebook_path}")
53 | notebook = Notebook(**model['content'])
54 | elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
55 | # Remote mode: use WebSocket connection to Y.js document
56 | async with notebook_manager.get_current_connection() as notebook_content:
57 | notebook = Notebook(**notebook_content.as_dict())
58 | else:
59 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}")
60 |
61 | if cell_index >= len(notebook):
62 | return f"Cell index {cell_index} is out of range. Notebook has {len(notebook)} cells."
63 | cell = notebook[cell_index]
64 | info_list = []
65 | # add cell metadata
66 | info_list.append(f"=====Cell {cell_index} | type: {cell.cell_type} | execution count: {cell.execution_count if cell.execution_count else 'N/A'}=====")
67 | # add cell source
68 | info_list.append(cell.get_source('readable'))
69 | # add cell outputs for code cells
70 | if cell.cell_type == "code" and include_outputs:
71 | info_list.extend(cell.get_outputs('readable'))
72 |
73 | return info_list
74 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | [build-system]
6 | requires = ["hatchling~=1.21"]
7 | build-backend = "hatchling.build"
8 |
9 | [project]
10 | name = "jupyter_mcp_server"
11 | authors = [{ name = "Datalayer", email = "info@datalayer.io" }]
12 | dynamic = ["version"]
13 | readme = "README.md"
14 | requires-python = ">=3.10"
15 | keywords = ["Jupyter"]
16 | classifiers = [
17 | "Intended Audience :: Developers",
18 | "Intended Audience :: System Administrators",
19 | "License :: OSI Approved :: BSD License",
20 | "Programming Language :: Python",
21 | "Programming Language :: Python :: 3",
22 | ]
23 | dependencies = [
24 | "jupyter-kernel-client>=0.7.3",
25 | "jupyter-mcp-tools>=0.1.4",
26 | "jupyter-nbmodel-client>=0.14.4",
27 | "jupyter-server-nbmodel>=0.1.1a4",
28 | "jupyter-server-client",
29 | "jupyter_server>=1.6,<3",
30 | "tornado>=6.1",
31 | "traitlets>=5.0",
32 | "mcp[cli]>=1.10.1",
33 | "pydantic",
34 | "uvicorn",
35 | "click",
36 | "fastapi"
37 | ]
38 |
39 | [project.optional-dependencies]
40 | test = [
41 | "ipykernel",
42 | "jupyter_server>=1.6,<3",
43 | "pytest>=7.0",
44 | "pytest-asyncio",
45 | "pytest-timeout>=2.1.0",
46 | "jupyterlab==4.4.1",
47 | "jupyter-collaboration==4.0.2",
48 | "datalayer_pycrdt==0.12.17",
49 | "pillow>=10.0.0"
50 | ]
51 | lint = ["mdformat>0.7", "mdformat-gfm>=0.3.5", "ruff"]
52 | typing = ["mypy>=0.990"]
53 |
54 | [project.scripts]
55 | jupyter-mcp-server = "jupyter_mcp_server.CLI:server"
56 |
57 | [project.license]
58 | file = "LICENSE"
59 |
60 | [project.urls]
61 | Home = "https://github.com/datalayer/jupyter-mcp-server"
62 |
63 | [tool.hatch.version]
64 | path = "jupyter_mcp_server/__version__.py"
65 |
66 | [tool.hatch.build]
67 | include = [
68 | "jupyter_mcp_server/**/*.py",
69 | "jupyter-config/**/*.json"
70 | ]
71 |
72 | [tool.hatch.build.targets.wheel.shared-data]
73 | "jupyter-config/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d"
74 | "jupyter-config/jupyter_notebook_config.d" = "etc/jupyter/jupyter_notebook_config.d"
75 |
76 | [tool.pytest.ini_options]
77 | filterwarnings = [
78 | "error",
79 | "ignore:There is no current event loop:DeprecationWarning",
80 | "module:make_current is deprecated:DeprecationWarning",
81 | "module:clear_current is deprecated:DeprecationWarning",
82 | "module:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning",
83 | ]
84 |
85 | [tool.mypy]
86 | check_untyped_defs = true
87 | disallow_incomplete_defs = true
88 | no_implicit_optional = true
89 | pretty = true
90 | show_error_context = true
91 | show_error_codes = true
92 | strict_equality = true
93 | warn_unused_configs = true
94 | warn_unused_ignores = true
95 | warn_redundant_casts = true
96 |
97 | [tool.ruff]
98 | target-version = "py310"
99 | line-length = 100
100 |
101 | [tool.ruff.lint]
102 | select = [
103 | "A",
104 | "B",
105 | "C",
106 | "E",
107 | "F",
108 | "FBT",
109 | "I",
110 | "N",
111 | "Q",
112 | "RUF",
113 | "S",
114 | "T",
115 | "UP",
116 | "W",
117 | "YTT",
118 | ]
119 | ignore = [
120 | # FBT001 Boolean positional arg in function definition
121 | "FBT001",
122 | "FBT002",
123 | "FBT003",
124 | ]
125 |
126 | [tool.ruff.lint.per-file-ignores]
127 | # S101 Use of `assert` detected
128 | "jupyter_mcp_server/tests/*" = ["S101"]
129 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # Contributor Covenant Code of Conduct
8 |
9 | ## Our Pledge
10 |
11 | We as members, contributors, and leaders pledge to make participation in our
12 | community a harassment-free experience for everyone, regardless of age, body
13 | size, visible or invisible disability, ethnicity, sex characteristics, gender
14 | identity and expression, level of experience, education, socio-economic status,
15 | nationality, personal appearance, race, religion, or sexual identity
16 | and orientation.
17 |
18 | We pledge to act and interact in ways that contribute to an open, welcoming,
19 | diverse, inclusive, and healthy community.
20 |
21 | ## Our Standards
22 |
23 | Examples of behavior that contributes to a positive environment for our
24 | community include:
25 |
26 | * Demonstrating empathy and kindness toward other people
27 | * Being respectful of differing opinions, viewpoints, and experiences
28 | * Giving and gracefully accepting constructive feedback
29 | * Accepting responsibility and apologizing to those affected by our mistakes,
30 | and learning from the experience
31 | * Focusing on what is best not just for us as individuals, but for the
32 | overall community
33 |
34 | Examples of unacceptable behavior include:
35 |
36 | * The use of sexualized language or imagery, and sexual attention or
37 | advances of any kind
38 | * Trolling, insulting or derogatory comments, and personal or political attacks
39 | * Public or private harassment
40 | * Publishing others' private information, such as a physical or email
41 | address, without their explicit permission
42 | * Other conduct which could reasonably be considered inappropriate in a
43 | professional setting
44 |
45 | ## Enforcement Responsibilities
46 |
47 | Community leaders are responsible for clarifying and enforcing our standards of
48 | acceptable behavior and will take appropriate and fair corrective action in
49 | response to any behavior that they deem inappropriate, threatening, offensive,
50 | or harmful.
51 |
52 | Community leaders have the right and responsibility to remove, edit, or reject
53 | comments, commits, code, wiki edits, issues, and other contributions that are
54 | not aligned to this Code of Conduct, and will communicate reasons for moderation
55 | decisions when appropriate.
56 |
57 | ## Scope
58 |
59 | This Code of Conduct applies within all community spaces, and also applies when
60 | an individual is officially representing the community in public spaces.
61 | Examples of representing our community include using an official e-mail address,
62 | posting via an official social media account, or acting as an appointed
63 | representative at an online or offline event.
64 |
65 | ## Enforcement
66 |
67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
68 | reported to the community leaders responsible for enforcement.
69 | All complaints will be reviewed and investigated promptly and fairly.
70 |
71 | All community leaders are obligated to respect the privacy and security of the
72 | reporter of any incident.
73 |
74 | ## Attribution
75 |
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
77 | version 2.0, available at
78 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
79 |
80 | [homepage]: https://www.contributor-covenant.org
81 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
82 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/restart_notebook_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Restart notebook tool implementation."""
6 |
7 | import logging
8 | from typing import Any, Optional
9 | from jupyter_server_client import JupyterServerClient
10 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode
11 | from jupyter_mcp_server.notebook_manager import NotebookManager
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | class RestartNotebookTool(BaseTool):
17 | """Tool to restart the kernel for a specific notebook."""
18 |
19 | async def execute(
20 | self,
21 | mode: ServerMode,
22 | server_client: Optional[JupyterServerClient] = None,
23 | kernel_client: Optional[Any] = None,
24 | contents_manager: Optional[Any] = None,
25 | kernel_manager: Optional[Any] = None,
26 | kernel_spec_manager: Optional[Any] = None,
27 | notebook_manager: Optional[NotebookManager] = None,
28 | # Tool-specific parameters
29 | notebook_name: str = None,
30 | **kwargs
31 | ) -> str:
32 | """Execute the restart_notebook tool.
33 |
34 | Args:
35 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
36 | kernel_manager: Kernel manager for JUPYTER_SERVER mode
37 | notebook_manager: Notebook manager instance
38 | notebook_name: Notebook identifier to restart
39 | **kwargs: Additional parameters
40 |
41 | Returns:
42 | Success message
43 | """
44 | if notebook_name not in notebook_manager:
45 | return f"Notebook '{notebook_name}' is not connected. All currently connected notebooks: {list(notebook_manager.list_all_notebooks().keys())}"
46 |
47 | if mode == ServerMode.JUPYTER_SERVER:
48 | # JUPYTER_SERVER mode: Use kernel_manager to restart the kernel
49 | if kernel_manager is None:
50 | return f"Failed to restart notebook '{notebook_name}': kernel_manager is required in JUPYTER_SERVER mode."
51 |
52 | # Get kernel ID from notebook_manager
53 | kernel_id = notebook_manager.get_kernel_id(notebook_name)
54 | if not kernel_id:
55 | return f"Failed to restart notebook '{notebook_name}': kernel ID not found."
56 |
57 | try:
58 | logger.info(f"Restarting kernel {kernel_id} for notebook '{notebook_name}' in JUPYTER_SERVER mode")
59 | await kernel_manager.restart_kernel(kernel_id)
60 | return f"Notebook '{notebook_name}' kernel restarted successfully. Memory state and imported packages have been cleared."
61 | except Exception as e:
62 | logger.error(f"Failed to restart kernel {kernel_id}: {e}")
63 | return f"Failed to restart notebook '{notebook_name}': {e}"
64 |
65 | elif mode == ServerMode.MCP_SERVER:
66 | # MCP_SERVER mode: Use notebook_manager's restart_notebook method
67 | success = notebook_manager.restart_notebook(notebook_name)
68 |
69 | if success:
70 | return f"Notebook '{notebook_name}' kernel restarted successfully. Memory state and imported packages have been cleared."
71 | else:
72 | return f"Failed to restart notebook '{notebook_name}'. The kernel may not support restart operation."
73 | else:
74 | return f"Invalid mode: {mode}"
75 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # 🚀 Jupyter MCP Server Release Guide
8 |
9 | This document provides detailed instructions on how to release the Jupyter MCP Server project using GitHub Actions.
10 |
11 | ## 📋 Release Process Overview
12 |
13 | The release process is defined in the `.github/workflows/release.yml` file and includes the following automated steps:
14 |
15 | 1. **Python Package Build** - Build distribution packages (wheel and source distribution)
16 | 2. **PyPI Publishing** - Automatically publish to PyPI (using OIDC trusted publishing)
17 | 3. **Docker Image Build** - Build multi-platform Docker images
18 | 4. **GitHub Release Creation** - Automatically generate release notes (simplified version, without automatic changelog)
19 |
20 | ## 🔖 Version Management
21 |
22 | ### Semantic Versioning
23 | The project uses semantic versioning format: `v{major}.{minor}.{patch}`
24 |
25 | Examples:
26 | - `v1.0.0` - Major version update
27 | - `v1.1.0` - Minor feature update
28 | - `v1.1.1` - Patch and bug fixes
29 |
30 | ### Version Tags
31 | For releases, you need to push Git tags with version numbers:
32 |
33 | ```bash
34 | # Create and push version tag
35 | git tag v1.0.0
36 | git push origin v1.0.0
37 | ```
38 |
39 | ## ⚙️ Release Workflow Trigger Conditions
40 |
41 | The workflow is automatically triggered when:
42 | - Pushing Git tags in the format `v*.*.*` (e.g., `v1.0.0`, `v2.1.3`)
43 |
44 | **Configuration Steps:**
45 |
46 | 1. Configure Trusted Publisher in PyPI project settings
47 | - Go to https://pypi.org/manage/project/jupyter-mcp-server/settings/publishing/
48 | - Add GitHub Actions as a trusted publisher
49 | - Configure:
50 | - Owner: `datalayer`
51 | - Repository: `jupyter-mcp-server`
52 | - Workflow: `release.yml`
53 | - Environment: `pypi`
54 |
55 | 2. GitHub repository settings
56 | - Settings → Environments → New environment: `pypi`
57 | - Configure protection rules (optional):
58 | - Required reviewers (requires approval)
59 | - Branch protection (only allows main branch)
60 |
61 | Creating access token in Docker Hub:
62 | - Settings → Security → Access Tokens
63 | - Create restricted token (read/write only specific repository)
64 | - Store as GitHub Secret:
65 | - `DOCKERHUB_USERNAME` - Docker Hub username
66 | - `DOCKERHUB_TOKEN` - Docker Hub access token
67 |
68 | ### GitHub Release
69 | - Uses built-in `GITHUB_TOKEN`, no additional configuration required
70 |
71 | ## 📦 Release Artifacts
72 |
73 | ### Python Package
74 | - **PyPI**: `https://pypi.org/project/jupyter-mcp-server/`
75 | - **Formats**: wheel (`.whl`) and source distribution (`.tar.gz`)
76 |
77 | ### Docker Image
78 | - **Repository**: `datalayer/jupyter-mcp-server`
79 | - **Tags**:
80 | - `latest` - Latest stable version
81 | - `{version}` - Specific version number (e.g., `1.0.0`)
82 |
83 | ## 🔄 Release Steps
84 |
85 | 1. **Update Version Number**
86 | ```bash
87 | # Edit version file
88 | vim jupyter_mcp_server/__version__.py
89 | ```
90 |
91 | 2. **Create Release Tag**
92 | ```bash
93 | git tag v1.0.0
94 | git push origin v1.0.0
95 | ```
96 |
97 | 3. **Monitor Release Progress**
98 | - Go to GitHub Actions page to view workflow run status
99 | - Check if PyPI has been updated
100 | - Verify Docker Hub has new images
101 |
102 | ## 🔄 Version Rollback
103 |
104 | If serious issues occur during release:
105 |
106 | 1. **Delete GitHub Release** (if created)
107 | 2. **Delete package from PyPI** (if published) - Requires admin privileges
108 | 3. **Delete Docker image tags from Docker Hub**
109 | 4. **Push new tag to re-release**
110 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tools/unuse_notebook_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Unuse notebook tool implementation."""
6 |
7 | import logging
8 | from typing import Any, Optional
9 | from jupyter_server_client import JupyterServerClient
10 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode
11 | from jupyter_mcp_server.notebook_manager import NotebookManager
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | class UnuseNotebookTool(BaseTool):
17 | """Tool to unuse from a notebook and release its resources"""
18 |
19 | async def execute(
20 | self,
21 | mode: ServerMode,
22 | server_client: Optional[JupyterServerClient] = None,
23 | kernel_client: Optional[Any] = None,
24 | contents_manager: Optional[Any] = None,
25 | kernel_manager: Optional[Any] = None,
26 | kernel_spec_manager: Optional[Any] = None,
27 | notebook_manager: Optional[NotebookManager] = None,
28 | # Tool-specific parameters
29 | notebook_name: str = None,
30 | **kwargs
31 | ) -> str:
32 | """Execute the unuse_notebook tool.
33 |
34 | Args:
35 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER)
36 | kernel_manager: Kernel manager for JUPYTER_SERVER mode (optional kernel shutdown)
37 | notebook_manager: Notebook manager instance
38 | notebook_name: Notebook identifier to disconnect
39 | **kwargs: Additional parameters
40 |
41 | Returns:
42 | Success message
43 | """
44 | if notebook_name not in notebook_manager:
45 | return f"Notebook '{notebook_name}' is not connected. All currently connected notebooks: {list(notebook_manager.list_all_notebooks().keys())}"
46 |
47 | # Get info about which notebook was current
48 | current_notebook = notebook_manager.get_current_notebook()
49 | was_current = current_notebook == notebook_name
50 |
51 | if mode == ServerMode.JUPYTER_SERVER:
52 | # JUPYTER_SERVER mode: Optionally shutdown kernel before removing
53 | # Note: In JUPYTER_SERVER mode, kernel lifecycle is managed by kernel_manager
54 | # We only remove the reference in notebook_manager, the actual kernel
55 | # continues to run unless explicitly shutdown
56 |
57 | kernel_id = notebook_manager.get_kernel_id(notebook_name)
58 | if kernel_id and kernel_manager:
59 | try:
60 | logger.info(f"Notebook '{notebook_name}' is being unused in JUPYTER_SERVER mode. Kernel {kernel_id} remains running.")
61 | # Optional: Uncomment to shutdown kernel when unused
62 | # await kernel_manager.shutdown_kernel(kernel_id)
63 | # logger.info(f"Kernel {kernel_id} shutdown successfully")
64 | except Exception as e:
65 | logger.warning(f"Note: Could not access kernel {kernel_id}: {e}")
66 |
67 | success = notebook_manager.remove_notebook(notebook_name)
68 |
69 | elif mode == ServerMode.MCP_SERVER:
70 | # MCP_SERVER mode: Use notebook_manager's remove_notebook method
71 | # which handles KernelClient cleanup automatically
72 | success = notebook_manager.remove_notebook(notebook_name)
73 | else:
74 | return f"Invalid mode: {mode}"
75 |
76 | if success:
77 | message = f"Notebook '{notebook_name}' unused successfully."
78 |
79 | if was_current:
80 | new_current = notebook_manager.get_current_notebook()
81 | if new_current:
82 | message += f" Current notebook switched to '{new_current}'."
83 | else:
84 | message += " No notebooks remaining."
85 |
86 | return message
87 | else:
88 | return f"Notebook '{notebook_name}' was not found."
89 |
--------------------------------------------------------------------------------
/docs/docs/jupyter/streamable-http/standalone/index.mdx:
--------------------------------------------------------------------------------
1 | # As a Standalone Server
2 |
3 | ## 1. Start JupyterLab
4 |
5 | ### Environment setup
6 |
7 | Make sure you have the following packages installed in your environment. The collaboration package is needed as the modifications made on the notebook can be seen thanks to [Jupyter Real Time Collaboration](https://jupyterlab.readthedocs.io/en/stable/user/rtc.html).
8 |
9 | ```bash
10 | pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 jupyter-mcp-tools>=0.1.4 ipykernel
11 | pip uninstall -y pycrdt datalayer_pycrdt
12 | pip install datalayer_pycrdt==0.12.17
13 | ```
14 |
15 | :::tip
16 | To confirm your environment is correctly configured:
17 | 1. Open a notebook in JupyterLab
18 | 2. Type some content in any cell (code or markdown)
19 | 3. Observe the tab indicator: you should see an "×" appear next to the notebook name, indicating unsaved changes
20 | 4. Wait a few seconds—the "×" should automatically change to a "●" without manually saving
21 |
22 | This automatic saving behavior confirms that the real-time collaboration features are working properly, which is essential for MCP server integration.
23 | :::
24 |
25 | ### JupyterLab start
26 |
27 | Then, start JupyterLab with the following command.
28 |
29 | ```bash
30 | jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN
31 | ```
32 |
33 | You can also run `make jupyterlab` if you cloned the repository.
34 |
35 | :::note
36 |
37 | If you wish to start the Jupyter MCP server using docker, add `--ip 0.0.0.0` to the jupyter lab command to allow the MCP Server running in a Docker container to access your local JupyterLab.
38 |
39 | :::
40 |
41 | :::info
42 | For JupyterHub:
43 | - Set the environment variable `JUPYTERHUB_ALLOW_TOKEN_IN_URL=1` in the single-user environment.
44 | - Ensure your API token (`MY_TOKEN`) is created with `access:servers` scope in the Hub.
45 | :::
46 |
47 | ## 2. Start Jupyter MCP Server
48 |
49 | The server runs on port `4040` and provides a streamable HTTP endpoint at `http://localhost:4040/mcp`.
50 |
51 | ### Using Python
52 |
53 | Install and start the server:
54 |
55 | ```bash
56 | pip install jupyter-mcp-server
57 | jupyter-mcp-server start \
58 | --transport streamable-http \
59 | --jupyter-url http://localhost:8888 \
60 | --jupyter-token MY_TOKEN \
61 | --port 4040
62 | ```
63 |
64 | ### Using Docker
65 |
66 | **MacOS/Windows:**
67 | ```bash
68 | docker run \
69 | -e JUPYTER_URL="http://host.docker.internal:8888" \
70 | -e JUPYTER_TOKEN="MY_TOKEN" \
71 | -p 4040:4040 \
72 | datalayer/jupyter-mcp-server:latest \
73 | --transport streamable-http
74 | ```
75 |
76 | **Linux:**
77 | ```bash
78 | docker run \
79 | --network=host \
80 | -e JUPYTER_URL="http://localhost:8888" \
81 | -e JUPYTER_TOKEN="MY_TOKEN" \
82 | -p 4040:4040 \
83 | datalayer/jupyter-mcp-server:latest \
84 | --transport streamable-http
85 | ```
86 |
87 | For advanced configuration options, see the [server configuration guide](/configuration).
88 |
89 |
100 |
101 | ## 3. Configure your MCP Client
102 |
103 | Use the following configuration to connect to the running server:
104 |
105 | ```json
106 | {
107 | "mcpServers": {
108 | "jupyter": {
109 | "command": "npx",
110 | "args": ["mcp-remote", "http://127.0.0.1:4040/mcp"]
111 | }
112 | }
113 | }
114 | ```
115 |
116 | ## Troubleshooting
117 |
118 | ### Common Issues
119 |
120 | **Connection refused:**
121 | - Verify the MCP server is running: `curl http://localhost:4040/mcp`
122 | - Check that port 4040 is not blocked by firewall
123 | - Ensure Docker port mapping is correct (`-p 4040:4040`)
124 |
125 | **Authentication errors:**
126 | - Verify `JUPYTER_TOKEN` matches your Jupyter server token
127 | - Check Jupyter server is accessible from MCP server
128 |
129 | For detailed configuration and troubleshooting, see the [configuration guide](/configuration).
130 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*' # Trigger condition: v0.15.2, v1.0.0, etc.
7 |
8 | permissions:
9 | contents: write
10 | id-token: write # For OIDC trusted publishing
11 |
12 | jobs:
13 | # Job 1: Build Python Package
14 | build-python:
15 | name: Build Python Package
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v5
19 |
20 | - name: Set up Python
21 | uses: actions/setup-python@v6
22 | with:
23 | python-version: '3.10'
24 |
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install build twine
29 |
30 | - name: Build package
31 | run: python -m build
32 |
33 | - name: Upload artifacts
34 | uses: actions/upload-artifact@v5
35 | with:
36 | name: python-package
37 | path: dist/*
38 |
39 | # Job 2: Publish to PyPI (using OIDC trusted publishing)
40 | publish-pypi:
41 | name: Publish to PyPI
42 | needs: build-python
43 | runs-on: ubuntu-latest
44 | environment:
45 | name: pypi
46 | url: https://pypi.org/p/jupyter-mcp-server
47 | permissions:
48 | id-token: write # Required for OIDC
49 | steps:
50 | - name: Download artifacts
51 | uses: actions/download-artifact@v6
52 | with:
53 | name: python-package
54 | path: dist
55 |
56 | - name: Publish to PyPI
57 | uses: pypa/gh-action-pypi-publish@release/v1
58 | with:
59 | # No password needed, using OIDC trusted publishing
60 | skip-existing: true
61 |
62 | # Job 3: Build and Publish Docker Image
63 | build-docker:
64 | name: Build and Push Docker Image
65 | runs-on: ubuntu-latest
66 | permissions:
67 | contents: read
68 | packages: write # For GHCR
69 | id-token: write # For Docker Hub OIDC (optional)
70 | steps:
71 | - uses: actions/checkout@v5
72 |
73 | - name: Extract version from tag
74 | id: version
75 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
76 |
77 | - name: Set up QEMU
78 | uses: docker/setup-qemu-action@v3
79 | with:
80 | platforms: arm64
81 |
82 | - name: Set up Docker Buildx
83 | uses: docker/setup-buildx-action@v3
84 |
85 | - name: Log in to Docker Hub
86 | uses: docker/login-action@v3
87 | with:
88 | username: ${{ secrets.DOCKERHUB_USERNAME }}
89 | password: ${{ secrets.DOCKERHUB_TOKEN }}
90 |
91 | - name: Build and push Docker image
92 | uses: docker/build-push-action@v6
93 | with:
94 | context: .
95 | platforms: linux/amd64,linux/arm64
96 | push: true
97 | tags: |
98 | datalayer/jupyter-mcp-server:latest
99 | datalayer/jupyter-mcp-server:${{ steps.version.outputs.VERSION }}
100 | cache-from: type=gha
101 | cache-to: type=gha,mode=max
102 |
103 | # Job 4: Create GitHub Release
104 | create-release:
105 | name: Create GitHub Release
106 | needs: [publish-pypi, build-docker]
107 | runs-on: ubuntu-latest
108 | permissions:
109 | contents: write
110 | steps:
111 | - uses: actions/checkout@v5
112 | with:
113 | fetch-depth: 0 # Fetch complete history for release creation
114 |
115 | - name: Extract version
116 | id: version
117 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
118 |
119 | - name: Create Release with Auto-Generated Notes
120 | run: |
121 | gh release create ${{ github.ref_name }} \
122 | --title "Release ${{ steps.version.outputs.VERSION }}" \
123 | --generate-notes \
124 | --notes "## 🚀 Release ${{ steps.version.outputs.VERSION }}
125 |
126 | ### 🔗 Links
127 |
128 | - [PyPI](https://pypi.org/project/jupyter-mcp-server/${{ steps.version.outputs.VERSION }}/)
129 | - [Docker Hub](https://hub.docker.com/r/datalayer/jupyter-mcp-server)"
130 | env:
131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
132 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/enroll.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """Auto-enrollment functionality for Jupyter MCP Server."""
6 |
7 | import logging
8 | from typing import Any
9 |
10 | from jupyter_mcp_server.notebook_manager import NotebookManager
11 | from jupyter_mcp_server.tools.use_notebook_tool import UseNotebookTool
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | async def auto_enroll_document(
17 | config: Any,
18 | notebook_manager: NotebookManager,
19 | use_notebook_tool: UseNotebookTool,
20 | server_context: Any,
21 | ) -> None:
22 | """Automatically enroll the configured document_id as a managed notebook.
23 |
24 | Handles kernel creation/connection based on configuration:
25 | - If runtime_id is provided: Connect to that specific kernel
26 | - If start_new_runtime is True: Create a new kernel
27 | - If both are False/None: Enroll notebook WITHOUT kernel (notebook-only mode)
28 |
29 | Args:
30 | config: JupyterMCPConfig instance with configuration parameters
31 | notebook_manager: NotebookManager instance for managing notebooks
32 | use_notebook_tool: UseNotebookTool instance for enrolling notebooks
33 | server_context: ServerContext instance with server state
34 | """
35 | # Check if document_id is configured and not already managed
36 | if not config.document_id:
37 | logger.debug("No document_id configured, skipping auto-enrollment")
38 | return
39 |
40 | if "default" in notebook_manager:
41 | logger.debug("Default notebook already enrolled, skipping auto-enrollment")
42 | return
43 |
44 | # Check if we should skip kernel creation entirely
45 | if not config.runtime_id and not config.start_new_runtime:
46 | # Enroll notebook without kernel - just register the notebook path
47 | try:
48 | logger.info(f"Auto-enrolling document '{config.document_id}' without kernel (notebook-only mode)")
49 | # Add notebook to manager without kernel
50 | notebook_manager.add_notebook(
51 | "default",
52 | None, # No kernel
53 | server_url=config.document_url,
54 | token=config.document_token,
55 | path=config.document_id
56 | )
57 | notebook_manager.set_current_notebook("default")
58 | logger.info(f"Auto-enrollment result: Successfully enrolled notebook 'default' at path '{config.document_id}' without kernel.")
59 | return
60 | except Exception as e:
61 | logger.warning(f"Failed to auto-enroll document without kernel: {e}")
62 | return
63 |
64 | # Otherwise, enroll with kernel
65 | try:
66 | # Determine kernel_id based on configuration
67 | kernel_id_to_use = None
68 | if config.runtime_id:
69 | # User explicitly provided a kernel ID to connect to
70 | kernel_id_to_use = config.runtime_id
71 | logger.info(f"Auto-enrolling document '{config.document_id}' with existing kernel '{kernel_id_to_use}'")
72 | elif config.start_new_runtime:
73 | # User wants a new kernel created
74 | kernel_id_to_use = None # Will trigger new kernel creation in use_notebook_tool
75 | logger.info(f"Auto-enrolling document '{config.document_id}' with new kernel")
76 |
77 | # Use the use_notebook_tool to properly enroll the notebook with kernel
78 | result = await use_notebook_tool.execute(
79 | mode=server_context.mode,
80 | server_client=server_context.server_client,
81 | notebook_name="default",
82 | notebook_path=config.document_id,
83 | use_mode="connect",
84 | kernel_id=kernel_id_to_use,
85 | contents_manager=server_context.contents_manager,
86 | kernel_manager=server_context.kernel_manager,
87 | session_manager=server_context.session_manager,
88 | notebook_manager=notebook_manager,
89 | runtime_url=config.runtime_url if config.runtime_url != "local" else None,
90 | runtime_token=config.runtime_token,
91 | )
92 | logger.info(f"Auto-enrollment result: {result}")
93 | except Exception as e:
94 | logger.warning(f"Failed to auto-enroll document: {e}. You can manually use it with use_notebook tool.")
95 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/models.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | from typing import Annotated, Optional, Literal
6 | from typing import Any
7 | from pydantic import BaseModel, Field
8 | from jupyter_mcp_server.utils import safe_extract_outputs, normalize_cell_source
9 |
10 |
11 | class DocumentRuntime(BaseModel):
12 | provider: str
13 | document_url: str
14 | document_id: str
15 | document_token: str
16 | runtime_url: str
17 | runtime_id: str
18 | runtime_token: str
19 |
20 | class Cell(BaseModel):
21 | """Notebook cell information as returned by the MCP server"""
22 |
23 | index: Annotated[int,Field(default=0)]
24 | cell_type: Annotated[Literal["raw", "code", "markdown"],Field(default="raw")]
25 | source: Annotated[Any,Field(default=[])]
26 | metadata: Annotated[Any,Field(default={})]
27 | id: Annotated[str,Field(default="")]
28 | execution_count: Annotated[Optional[int],Field(default=None)]
29 | outputs: Annotated[Any,Field(default=[])]
30 |
31 | def get_source(self, response_format: Literal['raw','readable'] = 'readable'):
32 | """Get the cell source in the requested format"""
33 | source = normalize_cell_source(self.source)
34 | if response_format == 'raw':
35 | return source
36 | elif response_format == 'readable':
37 | return "\n".join([line.rstrip("\n") for line in source])
38 |
39 | def get_outputs(self, response_format : Literal["raw",'readable']='readable'):
40 | """Get the cell output in the requested format"""
41 | if response_format == "raw":
42 | return self.outputs
43 | elif response_format == "readable":
44 | return safe_extract_outputs(self.outputs)
45 |
46 | def get_overview(self) -> str:
47 | """Get the cell overview(First Line and Lines)"""
48 | source = normalize_cell_source(self.source)
49 | if len(source) == 0:
50 | return ""
51 | first_line = source[0].rstrip("\n")
52 | if len(source) > 1:
53 | first_line += f"...({len(source) - 1} lines hidden)"
54 | return first_line
55 |
56 |
57 | class Notebook(BaseModel):
58 |
59 | cells: Annotated[list[Cell],Field(default=[])]
60 | metadata: Annotated[dict,Field(default={})]
61 | nbformat: Annotated[int,Field(default=4)]
62 | nbformat_minor: Annotated[int,Field(default=4)]
63 |
64 | def __len__(self) -> int:
65 | """Return the number of cells in the notebook"""
66 | return len(self.cells)
67 |
68 | def __getitem__(self, key) -> Cell | list[Cell]:
69 | """Support indexing and slicing operations on cells"""
70 | return self.cells[key]
71 |
72 | def format_output(self, response_format: Literal["brief", "detailed"] = "brief", start_index: int = 0, limit: int = 0):
73 | """
74 | Format notebook output based on response format and range parameters.
75 | Args:
76 | response_format: Format of the response ("brief" or "detailed")
77 | start_index: Starting index for cell range (default: 0)
78 | limit: Maximum number of cells to show (default: 0 means no limit)
79 | Returns:
80 | Formatted output string
81 | """
82 | # Determine the range of cells to display
83 | total_cells = len(self.cells)
84 | if total_cells == 0:
85 | return "Notebook is empty"
86 |
87 | # Calculate end index
88 | end_index = total_cells if limit == 0 else min(start_index + limit, total_cells)
89 | cells_to_show = self.cells[start_index:end_index]
90 | if len(cells_to_show) == 0:
91 | return "No cells in the specified range"
92 |
93 | if response_format == "brief":
94 | # Generate TSV table for brief format using get_overview
95 | from jupyter_mcp_server.utils import format_TSV
96 |
97 | headers = ["Index", "Type", "Count", "First Line"]
98 | rows = []
99 |
100 | for idx, cell in enumerate(cells_to_show):
101 | absolute_idx = start_index + idx
102 | cell_type = cell.cell_type
103 | execution_count = cell.execution_count if cell.execution_count else 'N/A'
104 | overview = cell.get_overview()
105 |
106 | rows.append([absolute_idx, cell_type, execution_count, overview])
107 |
108 | return format_TSV(headers, rows)
109 |
110 | elif response_format == "detailed":
111 | info_list = []
112 | for idx, cell in enumerate(cells_to_show):
113 | absolute_idx = start_index + idx
114 | info_list.append(f"=====Cell {absolute_idx} | type: {cell.cell_type} | execution count: {cell.execution_count if cell.execution_count else 'N/A'}=====\n")
115 | info_list.append(cell.get_source('readable'))
116 | info_list.append("\n\n")
117 |
118 | return "\n".join(info_list)
--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024- Datalayer, Inc.
3 | *
4 | * BSD 3-Clause License
5 | */
6 |
7 | /** @type {import('@docusaurus/types').DocusaurusConfig} */
8 | module.exports = {
9 | title: '🪐 ✨ Jupyter MCP Server documentation',
10 | tagline: 'Tansform your Notebooks into an interactive, AI-powered workspace that adapts to your needs!',
11 | url: 'https://datalayer.ai',
12 | baseUrl: '/',
13 | onBrokenLinks: 'throw',
14 | onBrokenMarkdownLinks: 'warn',
15 | favicon: 'img/favicon.ico',
16 | organizationName: 'datalayer', // Usually your GitHub org/user name.
17 | projectName: 'jupyter-mcp-server', // Usually your repo name.
18 | markdown: {
19 | mermaid: true,
20 | },
21 | plugins: [
22 | '@docusaurus/theme-live-codeblock',
23 | 'docusaurus-lunr-search',
24 | ],
25 | themes: [
26 | '@docusaurus/theme-mermaid',
27 | ],
28 | themeConfig: {
29 | colorMode: {
30 | defaultMode: 'light',
31 | disableSwitch: true,
32 | },
33 | navbar: {
34 | title: 'Jupyter MCP Server Docs',
35 | logo: {
36 | alt: 'Datalayer Logo',
37 | src: 'img/datalayer/logo.svg',
38 | },
39 | items: [
40 | {
41 | href: 'https://discord.gg/YQFwvmSSuR',
42 | position: 'right',
43 | className: 'header-discord-link',
44 | 'aria-label': 'Discord',
45 | },
46 | {
47 | href: 'https://github.com/datalayer/jupyter-mcp-server',
48 | position: 'right',
49 | className: 'header-github-link',
50 | 'aria-label': 'GitHub',
51 | },
52 | {
53 | href: 'https://bsky.app/profile/datalayer.ai',
54 | position: 'right',
55 | className: 'header-bluesky-link',
56 | 'aria-label': 'Bluesky',
57 | },
58 | {
59 | href: 'https://x.com/DatalayerIO',
60 | position: 'right',
61 | className: 'header-x-link',
62 | 'aria-label': 'X',
63 | },
64 | {
65 | href: 'https://www.linkedin.com/company/datalayer',
66 | position: 'right',
67 | className: 'header-linkedin-link',
68 | 'aria-label': 'LinkedIn',
69 | },
70 | {
71 | href: 'https://tiktok.com/@datalayerio',
72 | position: 'right',
73 | className: 'header-tiktok-link',
74 | 'aria-label': 'TikTok',
75 | },
76 | {
77 | href: 'https://www.youtube.com/@datalayer',
78 | position: 'right',
79 | className: 'header-youtube-link',
80 | 'aria-label': 'YouTube',
81 | },
82 | {
83 | href: 'https://datalayer.ai',
84 | position: 'right',
85 | className: 'header-datalayer-io-link',
86 | 'aria-label': 'Datalayer',
87 | },
88 | ],
89 | },
90 | footer: {
91 | style: 'dark',
92 | links: [
93 | {
94 | title: 'Docs',
95 | items: [
96 | {
97 | label: 'Jupyter MCP Server',
98 | to: '/',
99 | },
100 | ],
101 | },
102 | {
103 | title: 'Community',
104 | items: [
105 | {
106 | label: 'GitHub',
107 | href: 'https://github.com/datalayer',
108 | },
109 | {
110 | label: 'Bluesky',
111 | href: 'https://assets.datalayer.tech/logos-social-grey/youtube.svg',
112 | },
113 | {
114 | label: 'LinkedIn',
115 | href: 'https://www.linkedin.com/company/datalayer',
116 | },
117 | ],
118 | },
119 | {
120 | title: 'More',
121 | items: [
122 | {
123 | label: 'Datalayer',
124 | href: 'https://datalayer.ai',
125 | },
126 | {
127 | label: 'Datalayer Docs',
128 | href: 'https://docs.datalayer.app',
129 | },
130 | {
131 | label: 'Datalayer Guide',
132 | href: 'https://datalayer.guide',
133 | },
134 | {
135 | label: 'Datalayer Blog',
136 | href: 'https://datalayer.blog',
137 | },
138 | ],
139 | },
140 | ],
141 | copyright: `Copyright © ${new Date().getFullYear()} Datalayer, Inc.`,
142 | },
143 | },
144 | presets: [
145 | [
146 | '@docusaurus/preset-classic',
147 | {
148 | docs: {
149 | routeBasePath: '/',
150 | docItemComponent: '@theme/CustomDocItem',
151 | sidebarPath: require.resolve('./sidebars.js'),
152 | editUrl: 'https://github.com/datalayer/jupyter-mcp-server/edit/main/',
153 | },
154 | theme: {
155 | customCss: require.resolve('./src/css/custom.css'),
156 | },
157 | gtag: {
158 | trackingID: 'G-EYRGHH1GN6',
159 | anonymizeIP: false,
160 | },
161 | },
162 | ],
163 | ],
164 | };
165 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | SHELL=/bin/bash
6 |
7 | .DEFAULT_GOAL := default
8 |
9 | .PHONY: clean build
10 |
11 | VERSION = 0.2
12 |
13 | default: all ## default target is all
14 |
15 | help: ## display this help
16 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
17 |
18 | all: clean build ## clean and build
19 |
20 | install:
21 | pip install .
22 |
23 | dev:
24 | pip install ".[test,lint,typing]"
25 |
26 | test: ## run the unit tests
27 | git checkout ./dev/content && \
28 | TEST_MCP_SERVER=true \
29 | TEST_JUPYTER_SERVER=true \
30 | pytest
31 |
32 | test-mcp-server: ## run the unit tests for mcp server
33 | git checkout ./dev/content && \
34 | TEST_MCP_SERVER=true \
35 | TEST_JUPYTER_SERVER=false \
36 | pytest
37 |
38 | test-jupyter-server: ## run the unit tests for jupyter server
39 | git checkout ./dev/content && \
40 | TEST_MCP_SERVER=false \
41 | TEST_JUPYTER_SERVER=true \
42 | pytest
43 |
44 | test-integration: ## run the integration tests
45 | hatch test
46 |
47 | build:
48 | pip install build
49 | python -m build .
50 |
51 | clean: ## clean
52 | git clean -fdx
53 |
54 | build-docker: ## build the docker image
55 | docker buildx build --platform linux/amd64,linux/arm64 --push -t datalayer/jupyter-mcp-server:${VERSION} .
56 | docker buildx build --platform linux/amd64,linux/arm64 --push -t datalayer/jupyter-mcp-server:latest .
57 | # docker image tag datalayer/jupyter-mcp-server:${VERSION} datalayer/jupyter-mcp-server:latest
58 | @exec echo open https://hub.docker.com/r/datalayer/jupyter-mcp-server/tags
59 |
60 | start-docker: ## start the jupyter mcp server in docker
61 | docker run -i --rm \
62 | -e JUPYTER_URL=http://localhost:8888 \
63 | -e JUPYTER_TOKEN=MY_TOKEN \
64 | -e START_NEW_RUNTIME=true \
65 | --network=host \
66 | datalayer/jupyter-mcp-server:latest
67 |
68 | pull-docker: ## pull the latest docker image
69 | docker image pull datalayer/jupyter-mcp-server:latest
70 |
71 | push-docker: ## push the docker image to the registry
72 | docker push datalayer/jupyter-mcp-server:${VERSION}
73 | docker push datalayer/jupyter-mcp-server:latest
74 | @exec echo open https://hub.docker.com/r/datalayer/jupyter-mcp-server/tags
75 |
76 | claude-linux: ## run the claude desktop linux app using nix
77 | NIXPKGS_ALLOW_UNFREE=1 nix run github:k3d3/claude-desktop-linux-flake?rev=6d9eb2a653be8a6c06bc29a419839570e0ffc858 \
78 | --impure \
79 | --extra-experimental-features flakes \
80 | --extra-experimental-features nix-command
81 |
82 | start: ## start the jupyter mcp server with streamable-http transport
83 | @exec echo
84 | @exec echo curl http://localhost:4040/api/healthz
85 | @exec echo
86 | @exec echo 👉 Define in your favorite mcp client the server http://localhost:4040/mcp
87 | @exec echo
88 | jupyter-mcp-server start \
89 | --transport streamable-http \
90 | --jupyter-url http://localhost:8888 \
91 | --jupyter-token MY_TOKEN \
92 | --start-new-runtime true \
93 | --port 4040
94 |
95 | start-empty: ## start the jupyter mcp server with streamable-http transport and no document nor runtime
96 | @exec echo
97 | @exec echo curl http://localhost:4040/api/healthz
98 | @exec echo
99 | @exec echo 👉 Define in your favorite mcp client the server http://localhost:4040/mcp
100 | @exec echo
101 | jupyter-mcp-server start \
102 | --transport streamable-http \
103 | --jupyter-url http://localhost:8888 \
104 | --jupyter-token MY_TOKEN \
105 | --start-new-runtime false \
106 | --port 4040
107 |
108 | start-jupyter-server-extension: ## start jupyter server with MCP extension
109 | @exec echo
110 | @exec echo 🚀 Starting Jupyter Server with MCP Extension
111 | @exec echo 📍 Using local serverapp access - document_url=local, runtime_url=local
112 | @exec echo
113 | @exec echo 🔗 JupyterLab will be available at http://localhost:4040/lab
114 | @exec echo 🔗 MCP endpoints will be available at http://localhost:4040/mcp
115 | @exec echo
116 | @exec echo "Test with: curl http://localhost:4040/mcp/healthz"
117 | @exec echo
118 | jupyter lab \
119 | --JupyterMCPServerExtensionApp.document_url local \
120 | --JupyterMCPServerExtensionApp.runtime_url local \
121 | --JupyterMCPServerExtensionApp.document_id notebook.ipynb \
122 | --JupyterMCPServerExtensionApp.start_new_runtime True \
123 | --ServerApp.disable_check_xsrf True \
124 | --IdentityProvider.token MY_TOKEN \
125 | --ServerApp.root_dir ./dev/content \
126 | --port 4040
127 |
128 | jupyterlab: ## start jupyterlab for the mcp server
129 | pip uninstall -y pycrdt datalayer_pycrdt
130 | pip install datalayer_pycrdt
131 | @exec echo
132 | @exec echo curl http://localhost:8888/lab?token=MY_TOKEN
133 | @exec echo
134 | jupyter lab \
135 | --port 8888 \
136 | --ip 0.0.0.0 \
137 | --ServerApp.root_dir ./dev/content \
138 | --IdentityProvider.token MY_TOKEN
139 |
140 | publish-pypi: # publish the pypi package
141 | git clean -fdx && \
142 | python -m build
143 | @exec echo
144 | @exec echo twine upload ./dist/*-py3-none-any.whl
145 | @exec echo
146 | @exec echo https://pypi.org/project/jupyter-mcp-server/#history
147 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # Contributing to Jupyter MCP Server
8 |
9 | First off, thank you for considering contributing to Jupyter MCP Server! It's people like you that make this project great. Your contributions help us improve the project and make it more useful for everyone!
10 |
11 | ## Code of Conduct
12 |
13 | This project and everyone participating in it is governed by the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior.
14 |
15 | ## How Can I Contribute?
16 |
17 | We welcome contributions of all kinds, including:
18 | - 🐛 Bug fixes
19 | - 📝 Improvements to existing features or documentation
20 | - ✨ New feature development
21 |
22 | ### Reporting Bugs or Suggesting Enhancements
23 |
24 | Before creating a new issue, please **ensure one does not already exist** by searching on GitHub under [Issues](https://github.com/datalayer/jupyter-mcp-server/issues).
25 |
26 | - If you're reporting a bug, please include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
27 | - If you're suggesting an enhancement, clearly state the enhancement you are proposing and why it would be a good addition to the project.
28 |
29 | ## Development Setup
30 |
31 | To get started with development, you'll need to set up your environment.
32 |
33 | 1. **Clone the repository:**
34 | ```bash
35 | git clone https://github.com/datalayer/jupyter-mcp-server
36 | cd jupyter-mcp-server
37 | ```
38 |
39 | 2. **Install dependencies:**
40 | ```bash
41 | # Install the project in editable mode with test dependencies
42 | pip install -e ".[test]"
43 | ```
44 |
45 | 3. **Make Some Amazing Changes!**
46 | ```bash
47 | # Make some amazing changes to the source code!
48 | ```
49 |
50 | 4. **Run Tests:**
51 | ```bash
52 | make test
53 | ```
54 |
55 | 5. **Build Python Package/Docker Image:**
56 | ```bash
57 | # Build the Python package
58 | make build
59 | # Build the Docker image
60 | make build-docker
61 | ```
62 |
63 | ## Testing Guidelines
64 |
65 | This section provides comprehensive guidance for adding and maintaining tests in the Jupyter MCP Server project.
66 |
67 | ### Test Architecture
68 |
69 | The project supports testing in two deployment modes:
70 |
71 | 1. **MCP_SERVER Mode**: Standalone MCP server using HTTP/WebSocket to connect to Jupyter
72 | 2. **JUPYTER_SERVER Mode**: Jupyter extension with direct serverapp API access
73 |
74 | Tests are parametrized to run against both modes using the same MCPClient, ensuring consistent behavior across deployment patterns.
75 |
76 | ### Test Data
77 |
78 | Test notebooks are located in the `dev/content/` directory:
79 |
80 | - `notebook.ipynb`: Main test notebook with matplotlib examples and various cell types
81 | - `new.ipynb`: Additional test notebook for multi-notebook operations
82 |
83 | ### Tool Implementation and Output Matching
84 |
85 | When adding tests for new features or modifying existing tools, ensure your tests match the actual implementation in `jupyter_mcp_server/tools/`
86 |
87 | ## (Recommended) Manual Agent Testing
88 |
89 | 1. **Build Python Package:**
90 | ```bash
91 | make build
92 | ```
93 |
94 | 2. **Set Up Your Environment:**
95 | ```bash
96 | pip install jupyterlab==4.4.1 jupyter-collaboration==4.0.2 ipykernel
97 | pip uninstall -y pycrdt datalayer_pycrdt
98 | pip install datalayer_pycrdt==0.12.17
99 | ```
100 |
101 | 3. **Start Jupyter Server:**
102 | ```bash
103 | jupyter lab --port 8888 --IdentityProvider.token MY_TOKEN --ip 0.0.0.0
104 | ```
105 |
106 | 4. **Set Up Your MCP Client:**
107 | We recommend using `uvx` to start the MCP server, first install `uvx` with `pip install uv`.
108 |
109 | ```bash
110 | pip install uv
111 | uv --version
112 | # should be 0.6.14 or higher
113 | ```
114 |
115 | Then, set up your MCP client with the following configuration file.
116 |
117 | ```json
118 | {
119 | "mcpServers": {
120 | "Jupyter-MCP": {
121 | "command": "uvx",
122 | "args": [
123 | "--from",
124 | "your/path/to/jupyter-mcp-server/dist/jupyter_mcp_server-x.x.x-py3-none-any.whl",
125 | "jupyter-mcp-server"
126 | ],
127 | "env": {
128 | "JUPYTER_URL": "http://localhost:8888",
129 | "JUPYTER_TOKEN": "MY_TOKEN",
130 | "ALLOW_IMG_OUTPUT": "true"
131 | }
132 | }
133 | }
134 | }
135 | ```
136 |
137 | 5. **Test Your Changes:**
138 |
139 | You Can Test Your Changes with your favorite MCP client(e.g. Cursor, Gemini CLI, etc.).
140 |
141 | ## Pull Request Process
142 |
143 | 1. Once you are satisfied with your changes and tests, commit your code.
144 | 2. Push your branch to your fork and attach with detailed description of the changes you made.
145 | 3. Open a pull request to the `main` branch of the original repository.
146 |
147 | We look forward to your contributions!
148 |
--------------------------------------------------------------------------------
/tests/test_jupyter_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Integration tests for Jupyter MCP Server in JUPYTER_SERVER mode (extension).
7 |
8 | This test file validates the server when running as a Jupyter Server extension
9 | with direct access to serverapp resources (contents_manager, kernel_manager).
10 |
11 | Key differences from MCP_SERVER mode:
12 | - Uses YDoc collaborative editing when notebooks are open
13 | - Local file operations without HTTP roundtrip
14 |
15 | The tests connect to the extension's HTTP endpoints (not the standalone MCP server).
16 |
17 | Launch the tests:
18 | ```
19 | $ pytest tests/test_jupyter_extension.py -v
20 | ```
21 | """
22 |
23 | import logging
24 | from http import HTTPStatus
25 |
26 | import pytest
27 | import requests
28 |
29 | from .conftest import JUPYTER_TOKEN
30 |
31 |
32 | ###############################################################################
33 | # Unit Tests - Extension Components
34 | ###############################################################################
35 |
36 | def test_import():
37 | """Test that all extension imports work."""
38 | from jupyter_mcp_server.jupyter_extension import extension
39 | from jupyter_mcp_server.jupyter_extension import handlers
40 | from jupyter_mcp_server.jupyter_extension import context
41 | logging.info("✅ All imports successful")
42 | assert True
43 |
44 |
45 | def test_extension_points():
46 | """Test extension discovery."""
47 | from jupyter_mcp_server import _jupyter_server_extension_points
48 | points = _jupyter_server_extension_points()
49 | logging.info(f"Extension points: {points}")
50 | assert len(points) > 0
51 | assert "jupyter_mcp_server" in points[0]["module"]
52 |
53 |
54 | def test_handler_creation():
55 | """Test that handlers can be instantiated."""
56 | from jupyter_mcp_server.jupyter_extension.handlers import (
57 | MCPSSEHandler,
58 | MCPHealthHandler,
59 | MCPToolsListHandler
60 | )
61 | logging.info("✅ Handlers available")
62 | assert MCPSSEHandler is not None
63 | assert MCPHealthHandler is not None
64 | assert MCPToolsListHandler is not None
65 |
66 |
67 | ###############################################################################
68 | # Integration Tests - Extension Running in Jupyter
69 | ###############################################################################
70 |
71 | def test_extension_health(jupyter_server_with_extension):
72 | """Test that Jupyter server with MCP extension is healthy"""
73 | logging.info(f"Testing Jupyter+MCP extension health ({jupyter_server_with_extension})")
74 |
75 | # Test Jupyter API is accessible
76 | response = requests.get(
77 | f"{jupyter_server_with_extension}/api/status",
78 | headers={"Authorization": f"token {JUPYTER_TOKEN}"},
79 | )
80 | assert response.status_code == HTTPStatus.OK
81 | logging.info("✅ Jupyter API is accessible")
82 |
83 |
84 | def test_mode_comparison_documentation(jupyter_server_with_extension, jupyter_server):
85 | """
86 | Document the differences between the two server modes for future reference.
87 |
88 | This test serves as living documentation of the architecture.
89 | """
90 | logging.info("\n" + "="*80)
91 | logging.info("SERVER MODE COMPARISON")
92 | logging.info("="*80)
93 |
94 | logging.info("\nMCP_SERVER Mode (Standalone):")
95 | logging.info(f" - URL: {jupyter_server}")
96 | logging.info(" - Started via: python -m jupyter_mcp_server --transport streamable-http")
97 | logging.info(" - Tools use: JupyterServerClient + KernelClient (HTTP)")
98 | logging.info(" - File operations: HTTP API (contents API)")
99 | logging.info(" - Cell operations: WebSocket messages")
100 | logging.info(" - Execute IPython: WebSocket to kernel")
101 | logging.info(" - Tests: test_mcp_server.py")
102 |
103 | logging.info("\nJUPYTER_SERVER Mode (Extension):")
104 | logging.info(f" - URL: {jupyter_server_with_extension}")
105 | logging.info(" - Started via: jupyter lab --ServerApp.jpserver_extensions")
106 | logging.info(" - Tools use: Direct Python APIs (contents_manager, kernel_manager)")
107 | logging.info(" - File operations: Direct nbformat + YDoc collaborative")
108 | logging.info(" - Cell operations: YDoc when available, nbformat fallback")
109 | logging.info(" - Execute IPython: Direct kernel_manager.get_kernel() + ZMQ")
110 | logging.info(" - Tests: test_jupyter_extension.py (this file)")
111 |
112 | logging.info("\nKey Benefits of JUPYTER_SERVER Mode:")
113 | logging.info(" ✓ Real-time collaborative editing via YDoc")
114 | logging.info(" ✓ Zero-latency local operations")
115 | logging.info(" ✓ Direct ZMQ access to kernels")
116 | logging.info(" ✓ Automatic sync with JupyterLab UI")
117 |
118 | logging.info("\nKey Benefits of MCP_SERVER Mode:")
119 | logging.info(" ✓ Works with remote Jupyter servers")
120 | logging.info(" ✓ No Jupyter extension installation required")
121 | logging.info(" ✓ Can proxy to multiple Jupyter instances")
122 | logging.info(" ✓ Standard MCP protocol compatibility")
123 |
124 | logging.info("="*80 + "\n")
125 |
126 | # Both servers should be running
127 | assert jupyter_server is not None
128 | assert jupyter_server_with_extension is not None
129 | assert jupyter_server != jupyter_server_with_extension # Different ports
130 |
--------------------------------------------------------------------------------
/docs/static/img/datalayer/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/prompt/general/AGENT.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # Role
8 |
9 | You are a Jupyter Agent, a powerful AI assistant designed to help USER code in Jupyter Notebooks.
10 |
11 | You are pair programming with a USER to solve their coding task. Please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Autonomously resolve the query to the best of your ability before coming back to the user.
12 |
13 | Your main goal is to follow the USER's instructions at each message and deliver a high-quality Notebook with a clear structure.
14 |
15 | # Core Philosophy
16 |
17 | You are **Explorer, Not Builder**, your primary goal is to **explore, discover, and understand**. Treat your work as a scientific investigation, not a software engineering task. Your process should be iterative and guided by curiosity.
18 |
19 | ### View the Notebook as an Experimentation Space
20 |
21 | Treat the Notebook as more than just a document for Markdown and code cells. It is a complete, interactive experimentation space. This means you should leverage all its capabilities to explore and manipulate the environment, such as:
22 | - **Magic Commands**: Use magic commands to fully leverage the Jupyter's capabilities, such as `%pip install ` to manage dependencies.
23 | - **Shell Commands**: Execute shell commands directly in cells with `!`, for example, `!ls -l` to inspect files or `!pwd` to confirm the current directory.
24 |
25 | ### Embrace the Introspective Exploration Loop
26 |
27 | This is your core thinking process for any task. This cycle begins by deconstructing the user's request into a concrete, explorable problem and repeats until the goal is achieved.
28 |
29 | - **Observe and Formulate**: Observe the user's request and previous outputs. Analyze this information to formulate a specific, internal question that will guide your next immediate action.
30 | - **Code as the Hypothesis**: Write the minimal amount of code necessary to answer your internal question. This code acts as an experiment to test a hypothesis.
31 | - **Execute for Insight**: Run the code immediately. The output—whether a result, a plot, or an error—is the raw data from your experiment.
32 | - **Introspect and Iterate**: Analyze the output. What was learned? Does it answer your question? What new questions arise? Summarize your findings, and repeat the cycle, refining your understanding with each iteration.
33 |
34 | ## Context
35 |
36 | {{Add your custom context here, like your package installation, preferred code style, etc.}}
37 |
38 | # Rules
39 |
40 | 1. **ALWAYS MCP**: All operations on the Notebook, such as creating, editing, and code execution, MUST be performed via tools provided by Jupyter MCP. **NEVER Directly create or modify the Notebook Source File Content**.
41 | 2. **Prioritize Safety and Await Approval**: If a proposed step involves high risk (e.g., deleting files, modifying critical configurations) or high cost (e.g., downloading very large datasets, running long-lasting computations), you MUST terminate your work cycle, present the proposed action and its potential consequences to the USER, and await explicit approval before proceeding.
42 |
43 | # Notebook Format
44 |
45 | ## Overall Format
46 |
47 | 1. **Readability as a Story**: Your Notebook is not just a record of code execution; it's a narrative of your analytical journey and a powerful tool for sharing insights. Use Markdown cells strategically at key junctures to explain your thought process, justify decisions, interpret results, and guide the reader through your analysis.
48 | 2. **Maintain Tidiness**: Keep the Notebook clean, focused, and logically organized.
49 | - **Eliminate Redundancy**: Actively delete any unused, irrelevant, or redundant cells (both code and markdown) to maintain clarity and conciseness.
50 | - **Correct In-Place**: When a Code Cell execution results in an error, **ALWAYS modify the original cell to fix the error** rather than adding new cells below it. This ensures a clean, executable, and logical flow without cluttering the Notebook with failed attempts.
51 |
52 | ## Markdown Cell
53 |
54 | 1. Avoid large blocks of text; separate different logical blocks with blank lines. Prioritize the use of hierarchical headings (`##`, `###`) and bullet points (`-`) to organize content. Highlight important information with bold formatting (`**`).
55 | 2. Use LaTeX syntax for mathematical symbols and formulas. Enclose inline formulas with `$` (e.g., `$E=mc^2$`) and multi-line formulas with `$$` to ensure standard formatting.
56 |
57 | ### Example
58 | ```
59 | ## Data Preprocessing Steps
60 | This preprocessing includes 3 core steps:
61 | - **Missing Value Handling**: Use mean imputation for numerical features and mode imputation for categorical features.
62 | - **Outlier Detection**: Identify outliers outside the range `[-3σ, +3σ]` using the 3σ principle.
63 | - **Feature Scaling**: Perform standardization on continuous features with the formula:
64 | $$
65 | z = \frac{x - \mu}{\sigma}
66 | $$
67 | where $\mu$ is the mean and $\sigma$ is the standard deviation.
68 | ```
69 |
70 | ## Code Cell
71 | 1. Focus on a single verifiable function (e.g., "Import the pandas library and load the dataset", "Define a quadratic function solution formula"). Complex tasks must be split into multiple consecutive Cells and progressed step-by-step.
72 | 2. Each Code Cell must start with a functional comment that clearly states the core task of the Cell (e.g., `# Load the dataset and view the first 5 rows of data`).
73 |
74 | ### Example
75 | ```
76 | # Load the dataset and view basic information
77 |
78 | import pandas as pd
79 |
80 | data = pd.read_csv("user_behavior.csv")
81 |
82 | # Output the first 5 rows of data and data dimensions
83 | print(f"Dataset shape (rows, columns): {data.shape}")
84 | print("First 5 rows of the dataset:")
85 | data.head()
86 | ```
--------------------------------------------------------------------------------
/jupyter_mcp_server/config.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | import os
6 | from typing import Optional
7 | from pydantic import BaseModel, Field
8 |
9 |
10 | class JupyterMCPConfig(BaseModel):
11 | """Singleton configuration object for Jupyter MCP Server."""
12 |
13 | # Transport configuration
14 | transport: str = Field(default="stdio", description="The transport to use for the MCP server")
15 |
16 | # Provider configuration
17 | provider: str = Field(default="jupyter", description="The provider to use for the document and runtime")
18 |
19 | # Runtime configuration
20 | runtime_url: str = Field(default="http://localhost:8888", description="The runtime URL to use, or 'local' for direct serverapp access")
21 | start_new_runtime: bool = Field(default=False, description="Start a new runtime or use an existing one")
22 | runtime_id: Optional[str] = Field(default=None, description="The kernel ID to use")
23 | runtime_token: Optional[str] = Field(default=None, description="The runtime token to use for authentication")
24 |
25 | # Document configuration
26 | document_url: str = Field(default="http://localhost:8888", description="The document URL to use, or 'local' for direct serverapp access")
27 | document_id: Optional[str] = Field(default=None, description="The document id to use. Optional - if omitted, can list and select notebooks interactively")
28 | document_token: Optional[str] = Field(default=None, description="The document token to use for authentication")
29 |
30 | # Server configuration
31 | port: int = Field(default=4040, description="The port to use for the Streamable HTTP transport")
32 | jupyterlab: bool = Field(default=True, description="Enable JupyterLab mode (defaults to True)")
33 |
34 | class Config:
35 | """Pydantic configuration."""
36 | validate_assignment = True
37 | arbitrary_types_allowed = True
38 |
39 | def is_local_document(self) -> bool:
40 | """Check if document URL is set to local."""
41 | return self.document_url == "local"
42 |
43 | def is_local_runtime(self) -> bool:
44 | """Check if runtime URL is set to local."""
45 | return self.runtime_url == "local"
46 |
47 | def is_jupyterlab_mode(self) -> bool:
48 | """Check if JupyterLab mode is enabled."""
49 | return self.jupyterlab
50 |
51 | def _get_env_bool(env_name: str, default_value: bool = True) -> bool:
52 | """
53 | Get boolean value from environment variable, supporting multiple formats.
54 |
55 | Args:
56 | env_name: Environment variable name
57 | default_value: Default value
58 |
59 | Returns:
60 | bool: Boolean value
61 | """
62 | env_value = os.getenv(env_name)
63 | if env_value is None:
64 | return default_value
65 |
66 | # Supported true value formats
67 | true_values = {'true', '1', 'yes', 'on', 'enable', 'enabled'}
68 | # Supported false value formats
69 | false_values = {'false', '0', 'no', 'off', 'disable', 'disabled'}
70 |
71 | env_value_lower = env_value.lower().strip()
72 |
73 | if env_value_lower in true_values:
74 | return True
75 | elif env_value_lower in false_values:
76 | return False
77 | else:
78 | return default_value
79 |
80 | # Singleton instance
81 | _config_instance: Optional[JupyterMCPConfig] = None
82 | # Multimodal Output Configuration
83 | # Environment variable controls whether to return actual image content or text placeholder
84 | ALLOW_IMG_OUTPUT: bool = _get_env_bool("ALLOW_IMG_OUTPUT", True)
85 |
86 | def get_config() -> JupyterMCPConfig:
87 | """Get the singleton configuration instance."""
88 | global _config_instance
89 | if _config_instance is None:
90 | _config_instance = JupyterMCPConfig()
91 | return _config_instance
92 |
93 |
94 | def set_config(**kwargs) -> JupyterMCPConfig:
95 | """Set configuration values and return the config instance.
96 |
97 | Automatically handles string representations of None by removing them from kwargs,
98 | allowing defaults to be used instead. This handles cases where environment variables
99 | or MCP clients pass "None" as a string.
100 | """
101 | def should_skip(value):
102 | """Check if value is a string representation of None that should be skipped."""
103 | return isinstance(value, str) and value.lower() in ("none", "null", "")
104 |
105 | # Filter out string "None" values and let defaults be used instead
106 | # For optional fields (tokens, runtime_id, document_id), convert to actual None
107 | normalized_kwargs = {}
108 | for key, value in kwargs.items():
109 | if should_skip(value):
110 | # For optional fields, set to None; for required fields, skip (use default)
111 | if key in ("runtime_token", "document_token", "runtime_id", "document_id"):
112 | normalized_kwargs[key] = None
113 | # For required string fields like runtime_url, document_url, skip the key
114 | # to let the default value be used
115 | # Do nothing - skip this key
116 | else:
117 | normalized_kwargs[key] = value
118 |
119 | global _config_instance
120 | if _config_instance is None:
121 | _config_instance = JupyterMCPConfig(**normalized_kwargs)
122 | else:
123 | for key, value in normalized_kwargs.items():
124 | if hasattr(_config_instance, key):
125 | setattr(_config_instance, key, value)
126 | return _config_instance
127 |
128 |
129 | def reset_config() -> JupyterMCPConfig:
130 | """Reset configuration to defaults."""
131 | global _config_instance
132 | _config_instance = JupyterMCPConfig()
133 | return _config_instance
134 |
--------------------------------------------------------------------------------
/docs/docs/clients/vscode/index.mdx:
--------------------------------------------------------------------------------
1 | # VS Code
2 |
3 | You can find the complete VS Code MCP documentation [here](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_use-mcp-tools-in-agent-mode).
4 |
5 | ## Install VS Code
6 |
7 | Download VS Code from the [official site](https://code.visualstudio.com/Download) and install it.
8 |
9 | ## Install GitHub Copilot Extension
10 |
11 | To use MCP tools and Agent mode in VS Code, you need an active [GitHub Copilot](https://github.com/features/copilot) subscription. Then, install the [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extension from the VS Code Marketplace.
12 |
13 | ## Configure Jupyter MCP Server
14 |
15 | There are two ways to configure the Jupyter MCP Server in VS Code: user settings or workspace settings. Once configured, restart VS Code.
16 |
17 | :::note
18 |
19 | We explicitely use the name `DatalayerJupyter` as VS Code has already a `Jupyter` MCP Server configured by default for the VS Code built-in notebooks.
20 |
21 | :::
22 |
23 | ### As User Settings in `settings.json`
24 |
25 | Open your `settings.json`:
26 |
27 | - Press `Ctrl+Shift+P` (or `⌘⇧P` on macOS) to open the **Command Palette**
28 | - Type and select: **Preferences: Open Settings (JSON)**
29 | [Or click this command link inside VS Code](command:workbench.action.openSettingsJson)
30 |
31 | Then add the following configuration:
32 |
33 | **Simplified Configuration (Recommended):**
34 | ```jsonc
35 | {
36 | "mcp": {
37 | "servers": {
38 | "DatalayerJupyter": {
39 | "command": "docker",
40 | "args": [
41 | "run",
42 | "-i",
43 | "--rm",
44 | "-e",
45 | "JUPYTER_URL",
46 | "-e",
47 | "JUPYTER_TOKEN",
48 | "-e",
49 | "DOCUMENT_ID",
50 | "-e",
51 | "ALLOW_IMG_OUTPUT",
52 | "datalayer/jupyter-mcp-server:latest"
53 | ],
54 | "env": {
55 | "JUPYTER_URL": "http://host.docker.internal:8888",
56 | "JUPYTER_TOKEN": "MY_TOKEN",
57 | "DOCUMENT_ID": "notebook.ipynb",
58 | "ALLOW_IMG_OUTPUT": "true"
59 | }
60 | }
61 | }
62 | }
63 | ```
64 |
65 |
66 | Advanced Configuration (Optional)
67 |
68 | For advanced deployments with separate document and runtime servers:
69 |
70 | ```jsonc
71 | {
72 | "mcp": {
73 | "servers": {
74 | "DatalayerJupyter": {
75 | "command": "docker",
76 | "args": [
77 | "run",
78 | "-i",
79 | "--rm",
80 | "-e",
81 | "DOCUMENT_URL",
82 | "-e",
83 | "DOCUMENT_TOKEN",
84 | "-e",
85 | "DOCUMENT_ID",
86 | "-e",
87 | "RUNTIME_URL",
88 | "-e",
89 | "RUNTIME_TOKEN",
90 | "-e",
91 | "ALLOW_IMG_OUTPUT",
92 | "datalayer/jupyter-mcp-server:latest"
93 | ],
94 | "env": {
95 | "DOCUMENT_URL": "http://host.docker.internal:8888",
96 | "DOCUMENT_TOKEN": "MY_TOKEN",
97 | "DOCUMENT_ID": "notebook.ipynb",
98 | "RUNTIME_URL": "http://host.docker.internal:8888",
99 | "RUNTIME_TOKEN": "MY_TOKEN",
100 | "ALLOW_IMG_OUTPUT": "true"
101 | }
102 | }
103 | }
104 | }
105 | ```
106 |
107 |
108 |
109 | Update with the actual configuration details from the [Jupyter MCP Server configuration](/jupyter/stdio#2-setup-jupyter-mcp-server).
110 |
111 | ### As Workspace Settings in `.vscode/mcp.json`
112 |
113 | Open or create a `.vscode/mcp.json` file in your workspace root directory. Then add the following example configuration:
114 |
115 | **Simplified Configuration (Recommended):**
116 | ```jsonc
117 | {
118 | "servers": {
119 | "DatalayerJupyter": {
120 | "command": "docker",
121 | "args": [
122 | "run",
123 | "-i",
124 | "--rm",
125 | "-e",
126 | "JUPYTER_URL",
127 | "-e",
128 | "JUPYTER_TOKEN",
129 | "-e",
130 | "DOCUMENT_ID",
131 | "-e",
132 | "ALLOW_IMG_OUTPUT",
133 | "datalayer/jupyter-mcp-server:latest"
134 | ],
135 | "env": {
136 | "JUPYTER_URL": "http://host.docker.internal:8888",
137 | "JUPYTER_TOKEN": "MY_TOKEN",
138 | "DOCUMENT_ID": "notebook.ipynb",
139 | "ALLOW_IMG_OUTPUT": "true"
140 | }
141 | }
142 | }
143 | }
144 | ```
145 |
146 |
147 | Advanced Configuration (Optional)
148 |
149 | For advanced deployments with separate document and runtime servers:
150 |
151 | ```jsonc
152 | {
153 | "servers": {
154 | "DatalayerJupyter": {
155 | "command": "docker",
156 | "args": [
157 | "run",
158 | "-i",
159 | "--rm",
160 | "-e",
161 | "DOCUMENT_URL",
162 | "-e",
163 | "DOCUMENT_TOKEN",
164 | "-e",
165 | "DOCUMENT_ID",
166 | "-e",
167 | "RUNTIME_URL",
168 | "-e",
169 | "RUNTIME_TOKEN",
170 | "-e",
171 | "ALLOW_IMG_OUTPUT",
172 | "datalayer/jupyter-mcp-server:latest"
173 | ],
174 | "env": {
175 | "DOCUMENT_URL": "http://host.docker.internal:8888",
176 | "DOCUMENT_TOKEN": "MY_TOKEN",
177 | "DOCUMENT_ID": "notebook.ipynb",
178 | "RUNTIME_URL": "http://host.docker.internal:8888",
179 | "RUNTIME_TOKEN": "MY_TOKEN",
180 | "ALLOW_IMG_OUTPUT": "true"
181 | }
182 | }
183 | }
184 | }
185 | ```
186 |
187 |
188 |
189 | Update with the actual configuration details from the [Jupyter MCP Server configuration](/jupyter/stdio#2-setup-jupyter-mcp-server).
190 |
191 | This enables workspace-specific configuration and sharing.
192 |
193 | ## Use MCP Tools in Agent Mode
194 |
195 | 1. Launch Copilot Chat (`Ctrl+Alt+I` / `⌃⌘I`)
196 | 2. Switch to **Agent** mode from the dropdown
197 | 3. Click the **Tools** ⚙️ icon to manage Jupyter MCP Server tools
198 | 4. Use `#toolName` to invoke tools manually, or let Copilot invoke them automatically
199 | 5. Confirm tool actions when prompted (once or always)
200 |
--------------------------------------------------------------------------------
/docs/static/img/feature_2.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
141 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/server_context.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Singleton to cache server mode and context managers.
7 | """
8 |
9 | from jupyter_mcp_server.config import get_config
10 | from jupyter_mcp_server.log import logger
11 | from jupyter_mcp_server.tools import ServerMode
12 | from jupyter_server_client import JupyterServerClient
13 |
14 |
15 | class ServerContext:
16 | """Singleton to cache server mode and context managers."""
17 | _instance = None
18 | _mode = None
19 | _contents_manager = None
20 | _kernel_manager = None
21 | _kernel_spec_manager = None
22 | _session_manager = None
23 | _server_client = None
24 | _kernel_client = None
25 | _initialized = False
26 |
27 | @classmethod
28 | def get_instance(cls):
29 | if cls._instance is None:
30 | cls._instance = cls()
31 | return cls._instance
32 |
33 | @classmethod
34 | def reset(cls):
35 | """Reset the singleton instance. Use this when config changes."""
36 | if cls._instance is not None:
37 | cls._instance._initialized = False
38 | cls._instance._mode = None
39 | cls._instance._contents_manager = None
40 | cls._instance._kernel_manager = None
41 | cls._instance._kernel_spec_manager = None
42 | cls._instance._session_manager = None
43 | cls._instance._server_client = None
44 | cls._instance._kernel_client = None
45 |
46 | def initialize(self):
47 | """Initialize context once."""
48 | if self._initialized:
49 | return
50 |
51 | try:
52 | from jupyter_mcp_server.jupyter_extension.context import get_server_context
53 | context = get_server_context()
54 |
55 | if context.is_local_document() and context.get_contents_manager() is not None:
56 | self._mode = ServerMode.JUPYTER_SERVER
57 | self._contents_manager = context.get_contents_manager()
58 | self._kernel_manager = context.get_kernel_manager()
59 | self._kernel_spec_manager = context.get_kernel_spec_manager() if hasattr(context, 'get_kernel_spec_manager') else None
60 | self._session_manager = context.get_session_manager() if hasattr(context, 'get_session_manager') else None
61 | else:
62 | self._mode = ServerMode.MCP_SERVER
63 | # Initialize HTTP clients for MCP_SERVER mode
64 | config = get_config()
65 |
66 | # Validate that runtime_url is set and not None/empty
67 | # Note: String "None" values should have been normalized by start_command()
68 | runtime_url = config.runtime_url
69 | if not runtime_url or runtime_url in ("None", "none", "null", ""):
70 | raise ValueError(
71 | f"runtime_url is not configured (current value: {repr(runtime_url)}). "
72 | "Please check:\n"
73 | "1. RUNTIME_URL environment variable is set correctly (not the string 'None')\n"
74 | "2. --runtime-url argument is provided when starting the server\n"
75 | "3. The MCP client configuration passes runtime_url correctly"
76 | )
77 |
78 | logger.info(f"Initializing MCP_SERVER mode with runtime_url: {runtime_url}")
79 | self._server_client = JupyterServerClient(base_url=runtime_url, token=config.runtime_token)
80 | # kernel_client will be created lazily when needed
81 | except (ImportError, Exception) as e:
82 | # If not in Jupyter context, use MCP_SERVER mode
83 | if not isinstance(e, ValueError):
84 | self._mode = ServerMode.MCP_SERVER
85 | # Initialize HTTP clients for MCP_SERVER mode
86 | config = get_config()
87 |
88 | # Validate that runtime_url is set and not None/empty
89 | # Note: String "None" values should have been normalized by start_command()
90 | runtime_url = config.runtime_url
91 | if not runtime_url or runtime_url in ("None", "none", "null", ""):
92 | raise ValueError(
93 | f"runtime_url is not configured (current value: {repr(runtime_url)}). "
94 | "Please check:\n"
95 | "1. RUNTIME_URL environment variable is set correctly (not the string 'None')\n"
96 | "2. --runtime-url argument is provided when starting the server\n"
97 | "3. The MCP client configuration passes runtime_url correctly"
98 | )
99 |
100 | logger.info(f"Initializing MCP_SERVER mode with runtime_url: {runtime_url}")
101 | self._server_client = JupyterServerClient(base_url=runtime_url, token=config.runtime_token)
102 | else:
103 | raise
104 |
105 | self._initialized = True
106 | logger.info(f"Server mode initialized: {self._mode}")
107 |
108 | @property
109 | def mode(self):
110 | if not self._initialized:
111 | self.initialize()
112 | return self._mode
113 |
114 | @property
115 | def contents_manager(self):
116 | if not self._initialized:
117 | self.initialize()
118 | return self._contents_manager
119 |
120 | @property
121 | def kernel_manager(self):
122 | if not self._initialized:
123 | self.initialize()
124 | return self._kernel_manager
125 |
126 | @property
127 | def kernel_spec_manager(self):
128 | if not self._initialized:
129 | self.initialize()
130 | return self._kernel_spec_manager
131 |
132 | @property
133 | def session_manager(self):
134 | if not self._initialized:
135 | self.initialize()
136 | return self._session_manager
137 |
138 | @property
139 | def server_client(self):
140 | if not self._initialized:
141 | self.initialize()
142 | return self._server_client
143 |
144 | @property
145 | def kernel_client(self):
146 | if not self._initialized:
147 | self.initialize()
148 | return self._kernel_client
149 |
150 | def is_jupyterlab_mode(self) -> bool:
151 | """Check if JupyterLab mode is enabled."""
152 | from jupyter_mcp_server.config import get_config
153 | config = get_config()
154 | return config.is_jupyterlab_mode()
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/backends/remote_backend.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Remote Backend Implementation
7 |
8 | This backend uses the existing jupyter_nbmodel_client, jupyter_kernel_client,
9 | and jupyter_server_client packages to connect to remote Jupyter servers.
10 |
11 | For MCP_SERVER mode, this maintains 100% backward compatibility with the existing implementation.
12 | """
13 |
14 | from typing import Optional, Any, Union, Literal
15 | from mcp.types import ImageContent
16 | from jupyter_mcp_server.jupyter_extension.backends.base import Backend
17 |
18 | # Note: This is a placeholder that delegates to existing server.py logic
19 | # The actual implementation will be refactored from server.py in a later step
20 | # For now, this establishes the pattern
21 |
22 |
23 | class RemoteBackend(Backend):
24 | """
25 | Backend that connects to remote Jupyter servers using HTTP/WebSocket APIs.
26 |
27 | Uses:
28 | - jupyter_nbmodel_client.NbModelClient for notebook operations
29 | - jupyter_kernel_client.KernelClient for kernel operations
30 | - jupyter_server_client.JupyterServerClient for server operations
31 | """
32 |
33 | def __init__(self, document_url: str, document_token: str, runtime_url: str, runtime_token: str):
34 | """
35 | Initialize remote backend.
36 |
37 | Args:
38 | document_url: URL of Jupyter server for document operations
39 | document_token: Authentication token for document server
40 | runtime_url: URL of Jupyter server for runtime operations
41 | runtime_token: Authentication token for runtime server
42 | """
43 | self.document_url = document_url
44 | self.document_token = document_token
45 | self.runtime_url = runtime_url
46 | self.runtime_token = runtime_token
47 |
48 | # Notebook operations
49 |
50 | async def get_notebook_content(self, path: str) -> dict[str, Any]:
51 | """Get notebook content via remote API."""
52 | # TODO: Implement using jupyter_server_client
53 | raise NotImplementedError("To be refactored from server.py")
54 |
55 | async def list_notebooks(self, path: str = "") -> list[str]:
56 | """List notebooks via remote API."""
57 | # TODO: Implement using jupyter_server_client
58 | raise NotImplementedError("To be refactored from server.py")
59 |
60 | async def notebook_exists(self, path: str) -> bool:
61 | """Check if notebook exists via remote API."""
62 | # TODO: Implement using jupyter_server_client
63 | raise NotImplementedError("To be refactored from server.py")
64 |
65 | async def create_notebook(self, path: str) -> dict[str, Any]:
66 | """Create notebook via remote API."""
67 | # TODO: Implement using jupyter_server_client
68 | raise NotImplementedError("To be refactored from server.py")
69 |
70 | # Cell operations
71 |
72 | async def read_cells(
73 | self,
74 | path: str,
75 | start_index: Optional[int] = None,
76 | end_index: Optional[int] = None
77 | ) -> list[dict[str, Any]]:
78 | """Read cells via nbmodel_client."""
79 | # TODO: Implement using jupyter_nbmodel_client
80 | raise NotImplementedError("To be refactored from server.py")
81 |
82 | async def append_cell(
83 | self,
84 | path: str,
85 | cell_type: Literal["code", "markdown"],
86 | source: Union[str, list[str]]
87 | ) -> int:
88 | """Append cell via nbmodel_client."""
89 | # TODO: Implement using jupyter_nbmodel_client
90 | raise NotImplementedError("To be refactored from server.py")
91 |
92 | async def insert_cell(
93 | self,
94 | path: str,
95 | cell_index: int,
96 | cell_type: Literal["code", "markdown"],
97 | source: Union[str, list[str]]
98 | ) -> int:
99 | """Insert cell via nbmodel_client."""
100 | # TODO: Implement using jupyter_nbmodel_client
101 | raise NotImplementedError("To be refactored from server.py")
102 |
103 | async def delete_cell(self, path: str, cell_index: int) -> None:
104 | """Delete cell via nbmodel_client."""
105 | # TODO: Implement using jupyter_nbmodel_client
106 | raise NotImplementedError("To be refactored from server.py")
107 |
108 | async def overwrite_cell(
109 | self,
110 | path: str,
111 | cell_index: int,
112 | new_source: Union[str, list[str]]
113 | ) -> tuple[str, str]:
114 | """Overwrite cell via nbmodel_client."""
115 | # TODO: Implement using jupyter_nbmodel_client
116 | raise NotImplementedError("To be refactored from server.py")
117 |
118 | # Kernel operations
119 |
120 | async def get_or_create_kernel(self, path: str, kernel_id: Optional[str] = None) -> str:
121 | """Get or create kernel via kernel_client."""
122 | # TODO: Implement using jupyter_kernel_client
123 | raise NotImplementedError("To be refactored from server.py")
124 |
125 | async def execute_cell(
126 | self,
127 | path: str,
128 | cell_index: int,
129 | kernel_id: str,
130 | timeout_seconds: int = 300
131 | ) -> list[Union[str, ImageContent]]:
132 | """Execute cell via kernel_client."""
133 | # TODO: Implement using jupyter_kernel_client
134 | raise NotImplementedError("To be refactored from server.py")
135 |
136 | async def interrupt_kernel(self, kernel_id: str) -> None:
137 | """Interrupt kernel via kernel_client."""
138 | # TODO: Implement using jupyter_kernel_client
139 | raise NotImplementedError("To be refactored from server.py")
140 |
141 | async def restart_kernel(self, kernel_id: str) -> None:
142 | """Restart kernel via kernel_client."""
143 | # TODO: Implement using jupyter_kernel_client
144 | raise NotImplementedError("To be refactored from server.py")
145 |
146 | async def shutdown_kernel(self, kernel_id: str) -> None:
147 | """Shutdown kernel via kernel_client."""
148 | # TODO: Implement using jupyter_kernel_client
149 | raise NotImplementedError("To be refactored from server.py")
150 |
151 | async def list_kernels(self) -> list[dict[str, Any]]:
152 | """List kernels via server API."""
153 | # TODO: Implement using jupyter_server_client
154 | raise NotImplementedError("To be refactored from server.py")
155 |
156 | async def kernel_exists(self, kernel_id: str) -> bool:
157 | """Check if kernel exists via server API."""
158 | # TODO: Implement using jupyter_server_client
159 | raise NotImplementedError("To be refactored from server.py")
160 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/tool_cache.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Tool Cache Module
7 |
8 | Provides caching for expensive jupyter-mcp-tools queries to improve performance.
9 | """
10 |
11 | import asyncio
12 | import time
13 | from typing import Dict, List, Optional, Any
14 | from dataclasses import dataclass
15 | from jupyter_mcp_server.log import logger
16 |
17 |
18 | @dataclass
19 | class CacheEntry:
20 | """Represents a cached entry with timestamp and data."""
21 | data: List[Dict[str, Any]]
22 | timestamp: float
23 |
24 | def is_expired(self, ttl_seconds: int) -> bool:
25 | """Check if the cache entry has expired."""
26 | return time.time() - self.timestamp > ttl_seconds
27 |
28 |
29 | class ToolCache:
30 | """
31 | Cache for jupyter-mcp-tools data with TTL support.
32 |
33 | This cache stores the complete tool data to avoid expensive get_tools() calls.
34 | """
35 |
36 | def __init__(self, default_ttl: int = 300): # 5 minutes default
37 | """
38 | Initialize the tool cache.
39 |
40 | Args:
41 | default_ttl: Default time-to-live in seconds for cache entries
42 | """
43 | self._cache: Dict[str, CacheEntry] = {}
44 | self._default_ttl = default_ttl
45 | self._lock = asyncio.Lock()
46 |
47 | def _make_cache_key(self, base_url: str, query: str) -> str:
48 | """Create a cache key from the request parameters."""
49 | # Use a simplified key based on base_url and query
50 | # Don't include token for security reasons
51 | return f"{base_url}:{query}"
52 |
53 | async def get_tools(
54 | self,
55 | base_url: str,
56 | token: str,
57 | query: str,
58 | enabled_only: bool = False,
59 | ttl_seconds: Optional[int] = None,
60 | fetch_func: Optional[Any] = None
61 | ) -> List[Dict[str, Any]]:
62 | """
63 | Get tools from cache or fetch them if not cached/expired.
64 |
65 | Args:
66 | base_url: Jupyter server base URL
67 | token: Authentication token
68 | query: Search query for tools
69 | enabled_only: Whether to return only enabled tools
70 | ttl_seconds: Custom TTL for this request (overrides default)
71 | fetch_func: Function to call if cache miss (should be jupyter_mcp_tools.get_tools)
72 |
73 | Returns:
74 | List of tool dictionaries
75 | """
76 | cache_key = self._make_cache_key(base_url, query)
77 | ttl = ttl_seconds or self._default_ttl
78 |
79 | async with self._lock:
80 | # Check if we have a valid cache entry
81 | if cache_key in self._cache:
82 | entry = self._cache[cache_key]
83 | if not entry.is_expired(ttl):
84 | logger.debug(f"Cache HIT for {cache_key} (age: {time.time() - entry.timestamp:.1f}s)")
85 | return entry.data
86 | else:
87 | logger.debug(f"Cache EXPIRED for {cache_key} (age: {time.time() - entry.timestamp:.1f}s)")
88 | del self._cache[cache_key]
89 | else:
90 | logger.debug(f"Cache MISS for {cache_key}")
91 |
92 | # Cache miss or expired - fetch fresh data
93 | if fetch_func is None:
94 | logger.warning("No fetch function provided for cache miss - returning empty list")
95 | return []
96 |
97 | try:
98 | logger.info(f"Fetching fresh tools from jupyter-mcp-tools (query: '{query}')")
99 | fresh_data = await fetch_func(
100 | base_url=base_url,
101 | token=token,
102 | query=query,
103 | enabled_only=enabled_only
104 | )
105 |
106 | # Store in cache
107 | async with self._lock:
108 | self._cache[cache_key] = CacheEntry(
109 | data=fresh_data,
110 | timestamp=time.time()
111 | )
112 |
113 | logger.info(f"Cached {len(fresh_data)} tools for key {cache_key}")
114 | return fresh_data
115 |
116 | except Exception as e:
117 | logger.error(f"Failed to fetch tools from jupyter-mcp-tools: {e}")
118 | # Return empty list on error to prevent cascading failures
119 | return []
120 |
121 | async def invalidate(self, base_url: str, query: str = None):
122 | """
123 | Invalidate cache entries.
124 |
125 | Args:
126 | base_url: Base URL to invalidate entries for
127 | query: Specific query to invalidate (if None, invalidates all for base_url)
128 | """
129 | async with self._lock:
130 | if query is None:
131 | # Invalidate all entries for this base_url
132 | keys_to_remove = [
133 | key for key in self._cache.keys()
134 | if key.startswith(f"{base_url}:")
135 | ]
136 | for key in keys_to_remove:
137 | del self._cache[key]
138 | logger.info(f"Invalidated {len(keys_to_remove)} cache entries for {base_url}")
139 | else:
140 | # Invalidate specific entry
141 | cache_key = self._make_cache_key(base_url, query)
142 | if cache_key in self._cache:
143 | del self._cache[cache_key]
144 | logger.info(f"Invalidated cache entry for {cache_key}")
145 |
146 | async def clear(self):
147 | """Clear all cache entries."""
148 | async with self._lock:
149 | count = len(self._cache)
150 | self._cache.clear()
151 | logger.info(f"Cleared {count} cache entries")
152 |
153 | def get_cache_stats(self) -> Dict[str, Any]:
154 | """Get cache statistics."""
155 | return {
156 | "total_entries": len(self._cache),
157 | "entries": [
158 | {
159 | "key": key,
160 | "age_seconds": time.time() - entry.timestamp,
161 | "expired": entry.is_expired(self._default_ttl),
162 | "data_count": len(entry.data)
163 | }
164 | for key, entry in self._cache.items()
165 | ]
166 | }
167 |
168 |
169 | # Global cache instance
170 | _global_tool_cache = None
171 |
172 |
173 | def get_tool_cache() -> ToolCache:
174 | """Get the global tool cache instance."""
175 | global _global_tool_cache
176 | if _global_tool_cache is None:
177 | _global_tool_cache = ToolCache(default_ttl=300) # 5 minutes
178 | return _global_tool_cache
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/context.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Server Context Management
7 |
8 | This module provides a singleton to track the execution context (MCP_SERVER vs JUPYTER_SERVER)
9 | and provide access to Jupyter Server resources when running as an extension.
10 | """
11 |
12 | from typing import Optional, Literal, TYPE_CHECKING
13 | import threading
14 |
15 | if TYPE_CHECKING:
16 | from jupyter_server.serverapp import ServerApp
17 |
18 |
19 | class ServerContext:
20 | """
21 | Singleton managing server execution context.
22 |
23 | This class tracks whether tools are running in standalone MCP_SERVER mode
24 | or embedded JUPYTER_SERVER mode, and provides access to server resources.
25 | """
26 |
27 | _instance: Optional['ServerContext'] = None
28 | _lock = threading.Lock()
29 |
30 | def __new__(cls):
31 | if cls._instance is None:
32 | with cls._lock:
33 | if cls._instance is None:
34 | cls._instance = super().__new__(cls)
35 | cls._instance._initialized = False
36 | return cls._instance
37 |
38 | def __init__(self):
39 | if self._initialized:
40 | return
41 |
42 | self._initialized = True
43 | self._context_type: Literal["MCP_SERVER", "JUPYTER_SERVER"] = "MCP_SERVER"
44 | self._serverapp: Optional['ServerApp'] = None
45 | self._document_url: Optional[str] = None
46 | self._runtime_url: Optional[str] = None
47 | self._jupyterlab: bool = True # Default to True
48 |
49 | @property
50 | def context_type(self) -> Literal["MCP_SERVER", "JUPYTER_SERVER"]:
51 | """Get the current server context type."""
52 | return self._context_type
53 |
54 | @property
55 | def serverapp(self) -> Optional['ServerApp']:
56 | """Get the Jupyter ServerApp instance (only available in JUPYTER_SERVER mode)."""
57 | return self._serverapp
58 |
59 | @property
60 | def document_url(self) -> Optional[str]:
61 | """Get the configured document URL."""
62 | return self._document_url
63 |
64 | @property
65 | def runtime_url(self) -> Optional[str]:
66 | """Get the configured runtime URL."""
67 | return self._runtime_url
68 |
69 | @property
70 | def jupyterlab(self) -> bool:
71 | """Get the jupyterlab mode flag."""
72 | return self._jupyterlab
73 |
74 | def update(
75 | self,
76 | context_type: Literal["MCP_SERVER", "JUPYTER_SERVER"],
77 | serverapp: Optional['ServerApp'] = None,
78 | document_url: Optional[str] = None,
79 | runtime_url: Optional[str] = None,
80 | jupyterlab: Optional[bool] = None
81 | ):
82 | """
83 | Update the server context.
84 |
85 | Args:
86 | context_type: The type of server context
87 | serverapp: Jupyter ServerApp instance (required for JUPYTER_SERVER mode)
88 | document_url: Document URL configuration
89 | runtime_url: Runtime URL configuration
90 | jupyterlab: JupyterLab mode flag (defaults to True when JUPYTER_SERVER mode is true)
91 | """
92 | with self._lock:
93 | self._context_type = context_type
94 | self._serverapp = serverapp
95 | self._document_url = document_url
96 | self._runtime_url = runtime_url
97 |
98 | # Set jupyterlab flag - default to True if JUPYTER_SERVER mode, otherwise keep current value
99 | if jupyterlab is not None:
100 | self._jupyterlab = jupyterlab
101 | elif context_type == "JUPYTER_SERVER":
102 | self._jupyterlab = True # Default to True for JUPYTER_SERVER mode
103 |
104 | if context_type == "JUPYTER_SERVER" and serverapp is None:
105 | raise ValueError("serverapp is required when context_type is JUPYTER_SERVER")
106 |
107 | def is_local_document(self) -> bool:
108 | """Check if document operations should use local serverapp."""
109 | return (
110 | self._context_type == "JUPYTER_SERVER"
111 | and self._document_url == "local"
112 | )
113 |
114 | def is_local_runtime(self) -> bool:
115 | """Check if runtime operations should use local serverapp."""
116 | return (
117 | self._context_type == "JUPYTER_SERVER"
118 | and self._runtime_url == "local"
119 | )
120 |
121 | def is_jupyterlab_mode(self) -> bool:
122 | """Check if JupyterLab mode is enabled."""
123 | return self._jupyterlab
124 |
125 | def get_contents_manager(self):
126 | """
127 | Get the Jupyter contents manager (only available in JUPYTER_SERVER mode with local access).
128 |
129 | Returns:
130 | ContentsManager instance or None
131 | """
132 | if self._serverapp is not None:
133 | return self._serverapp.contents_manager
134 | return None
135 |
136 | def get_kernel_manager(self):
137 | """
138 | Get the Jupyter kernel manager (only available in JUPYTER_SERVER mode with local access).
139 |
140 | Returns:
141 | KernelManager instance or None
142 | """
143 | if self._serverapp is not None:
144 | return self._serverapp.kernel_manager
145 | return None
146 |
147 | def get_kernel_spec_manager(self):
148 | """
149 | Get the Jupyter kernel spec manager (only available in JUPYTER_SERVER mode with local access).
150 |
151 | Returns:
152 | KernelSpecManager instance or None
153 | """
154 | if self._serverapp is not None:
155 | return self._serverapp.kernel_spec_manager
156 | return None
157 |
158 | def get_session_manager(self):
159 | """
160 | Get the Jupyter session manager (only available in JUPYTER_SERVER mode with local access).
161 |
162 | Returns:
163 | SessionManager instance or None
164 | """
165 | if self._serverapp is not None:
166 | return self._serverapp.session_manager
167 | return None
168 |
169 | @property
170 | def session_manager(self):
171 | """
172 | Get the Jupyter session manager as a property (only available in JUPYTER_SERVER mode with local access).
173 |
174 | Returns:
175 | SessionManager instance or None
176 | """
177 | return self.get_session_manager()
178 |
179 | def reset(self):
180 | """Reset to default MCP_SERVER mode."""
181 | with self._lock:
182 | self._context_type = "MCP_SERVER"
183 | self._serverapp = None
184 | self._document_url = None
185 | self._runtime_url = None
186 | self._jupyterlab = True # Default to True
187 |
188 |
189 | # Global accessor
190 | def get_server_context() -> ServerContext:
191 | """Get the global ServerContext singleton instance."""
192 | return ServerContext()
193 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/protocol/messages.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | MCP Protocol Messages
7 |
8 | Pydantic models for MCP protocol requests and responses to ensure consistent
9 | API across both MCP_SERVER and JUPYTER_SERVER modes.
10 | """
11 |
12 | from typing import Any, Optional, Union, Literal
13 | from pydantic import BaseModel, Field
14 | from mcp.types import ImageContent
15 |
16 |
17 | # Tool execution models
18 | class ToolRequest(BaseModel):
19 | """Request to execute a tool"""
20 | tool_name: str = Field(..., description="Name of the tool to execute")
21 | arguments: dict[str, Any] = Field(default_factory=dict, description="Tool arguments")
22 | context: Optional[dict[str, Any]] = Field(None, description="Execution context")
23 |
24 |
25 | class ToolResponse(BaseModel):
26 | """Response from tool execution"""
27 | success: bool = Field(..., description="Whether execution was successful")
28 | result: Any = Field(None, description="Tool execution result")
29 | error: Optional[str] = Field(None, description="Error message if execution failed")
30 |
31 |
32 | # Notebook operation models
33 | class NotebookContentRequest(BaseModel):
34 | """Request to retrieve notebook content"""
35 | path: str = Field(..., description="Path to the notebook file")
36 | include_outputs: bool = Field(True, description="Include cell outputs")
37 |
38 |
39 | class NotebookContentResponse(BaseModel):
40 | """Response containing notebook content"""
41 | path: str = Field(..., description="Notebook path")
42 | cells: list[dict[str, Any]] = Field(..., description="List of cells")
43 | metadata: dict[str, Any] = Field(default_factory=dict, description="Notebook metadata")
44 |
45 |
46 | class NotebookListRequest(BaseModel):
47 | """Request to list notebooks"""
48 | path: Optional[str] = Field("", description="Directory path to search")
49 | recursive: bool = Field(True, description="Search recursively")
50 |
51 |
52 | class NotebookListResponse(BaseModel):
53 | """Response containing list of notebooks"""
54 | notebooks: list[str] = Field(..., description="List of notebook paths")
55 |
56 |
57 | # Cell operation models
58 | class ReadCellsRequest(BaseModel):
59 | """Request to read cells from a notebook"""
60 | path: Optional[str] = Field(None, description="Notebook path (uses current if not specified)")
61 | start_index: Optional[int] = Field(None, description="Start cell index")
62 | end_index: Optional[int] = Field(None, description="End cell index")
63 |
64 |
65 | class ReadCellsResponse(BaseModel):
66 | """Response containing cell information"""
67 | cells: list[dict[str, Any]] = Field(..., description="List of cell information")
68 |
69 |
70 | class AppendCellRequest(BaseModel):
71 | """Request to append a cell"""
72 | path: Optional[str] = Field(None, description="Notebook path")
73 | cell_type: Literal["code", "markdown"] = Field(..., description="Cell type")
74 | source: Union[str, list[str]] = Field(..., description="Cell source")
75 |
76 |
77 | class AppendCellResponse(BaseModel):
78 | """Response after appending a cell"""
79 | cell_index: int = Field(..., description="Index of the appended cell")
80 | message: str = Field(..., description="Success message")
81 |
82 |
83 | class InsertCellRequest(BaseModel):
84 | """Request to insert a cell"""
85 | path: Optional[str] = Field(None, description="Notebook path")
86 | cell_index: int = Field(..., description="Index where to insert")
87 | cell_type: Literal["code", "markdown"] = Field(..., description="Cell type")
88 | source: Union[str, list[str]] = Field(..., description="Cell source")
89 |
90 |
91 | class InsertCellResponse(BaseModel):
92 | """Response after inserting a cell"""
93 | cell_index: int = Field(..., description="Index of the inserted cell")
94 | message: str = Field(..., description="Success message")
95 |
96 |
97 | class DeleteCellRequest(BaseModel):
98 | """Request to delete a cell"""
99 | path: Optional[str] = Field(None, description="Notebook path")
100 | cell_index: int = Field(..., description="Index of cell to delete")
101 |
102 |
103 | class DeleteCellResponse(BaseModel):
104 | """Response after deleting a cell"""
105 | message: str = Field(..., description="Success message")
106 |
107 |
108 | class OverwriteCellRequest(BaseModel):
109 | """Request to overwrite a cell"""
110 | path: Optional[str] = Field(None, description="Notebook path")
111 | cell_index: int = Field(..., description="Index of cell to overwrite")
112 | new_source: Union[str, list[str]] = Field(..., description="New cell source")
113 |
114 |
115 | class OverwriteCellResponse(BaseModel):
116 | """Response after overwriting a cell"""
117 | message: str = Field(..., description="Success message with diff")
118 |
119 |
120 | # Cell execution models
121 | class ExecuteCellRequest(BaseModel):
122 | """Request to execute a cell"""
123 | path: Optional[str] = Field(None, description="Notebook path")
124 | cell_index: int = Field(..., description="Index of cell to execute")
125 | timeout_seconds: int = Field(300, description="Execution timeout in seconds")
126 |
127 |
128 | class ExecuteCellResponse(BaseModel):
129 | """Response after executing a cell"""
130 | cell_index: int = Field(..., description="Executed cell index")
131 | outputs: list[Union[str, ImageContent]] = Field(..., description="Cell outputs")
132 | execution_count: Optional[int] = Field(None, description="Execution count")
133 | status: Literal["success", "error", "timeout"] = Field(..., description="Execution status")
134 |
135 |
136 | # Kernel operation models
137 | class ConnectNotebookRequest(BaseModel):
138 | """Request to connect to a notebook"""
139 | notebook_name: str = Field(..., description="Unique notebook identifier")
140 | notebook_path: str = Field(..., description="Path to notebook file")
141 | mode: Literal["connect", "create"] = Field("connect", description="Connection mode")
142 | kernel_id: Optional[str] = Field(None, description="Specific kernel ID")
143 |
144 |
145 | class ConnectNotebookResponse(BaseModel):
146 | """Response after connecting to notebook"""
147 | message: str = Field(..., description="Success message")
148 | notebook_name: str = Field(..., description="Notebook identifier")
149 | notebook_path: str = Field(..., description="Notebook path")
150 |
151 |
152 | class UnuseNotebookRequest(BaseModel):
153 | """Request to unuse from a notebook"""
154 | notebook_name: str = Field(..., description="Notebook identifier to disconnect")
155 |
156 |
157 | class UnuseNotebookResponse(BaseModel):
158 | """Response after disconnecting"""
159 | message: str = Field(..., description="Success message")
160 |
161 |
162 | class RestartNotebookRequest(BaseModel):
163 | """Request to restart a notebook kernel"""
164 | notebook_name: str = Field(..., description="Notebook identifier to restart")
165 |
166 |
167 | class RestartNotebookResponse(BaseModel):
168 | """Response after restarting kernel"""
169 | message: str = Field(..., description="Success message")
170 | notebook_name: str = Field(..., description="Notebook identifier")
171 |
--------------------------------------------------------------------------------
/jupyter_mcp_server/jupyter_extension/backends/base.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024- Datalayer, Inc.
2 | #
3 | # BSD 3-Clause License
4 |
5 | """
6 | Abstract Backend Interface
7 |
8 | Defines the contract for all backend implementations (Remote and Local).
9 | """
10 |
11 | from abc import ABC, abstractmethod
12 | from typing import Optional, Any, Union, Literal
13 | from mcp.types import ImageContent
14 |
15 |
16 | class Backend(ABC):
17 | """
18 | Abstract backend for notebook and kernel operations.
19 |
20 | Implementations:
21 | - RemoteBackend: Uses jupyter_nbmodel_client, jupyter_kernel_client, jupyter_server_client
22 | - LocalBackend: Uses local serverapp.contents_manager and serverapp.kernel_manager
23 | """
24 |
25 | # Notebook operations
26 |
27 | @abstractmethod
28 | async def get_notebook_content(self, path: str) -> dict[str, Any]:
29 | """
30 | Retrieve notebook content.
31 |
32 | Args:
33 | path: Path to the notebook file
34 |
35 | Returns:
36 | Dictionary with notebook content (cells, metadata)
37 | """
38 | pass
39 |
40 | @abstractmethod
41 | async def list_notebooks(self, path: str = "") -> list[str]:
42 | """
43 | List all notebooks in a directory.
44 |
45 | Args:
46 | path: Directory path (empty string for root)
47 |
48 | Returns:
49 | List of notebook paths
50 | """
51 | pass
52 |
53 | @abstractmethod
54 | async def notebook_exists(self, path: str) -> bool:
55 | """
56 | Check if a notebook exists.
57 |
58 | Args:
59 | path: Path to the notebook file
60 |
61 | Returns:
62 | True if notebook exists
63 | """
64 | pass
65 |
66 | @abstractmethod
67 | async def create_notebook(self, path: str) -> dict[str, Any]:
68 | """
69 | Create a new notebook.
70 |
71 | Args:
72 | path: Path for the new notebook
73 |
74 | Returns:
75 | Created notebook content
76 | """
77 | pass
78 |
79 | # Cell operations (via notebook connection)
80 |
81 | @abstractmethod
82 | async def read_cells(
83 | self,
84 | path: str,
85 | start_index: Optional[int] = None,
86 | end_index: Optional[int] = None
87 | ) -> list[dict[str, Any]]:
88 | """
89 | Read cells from a notebook.
90 |
91 | Args:
92 | path: Notebook path
93 | start_index: Start cell index (None for all)
94 | end_index: End cell index (None for all)
95 |
96 | Returns:
97 | List of cell dictionaries
98 | """
99 | pass
100 |
101 | @abstractmethod
102 | async def append_cell(
103 | self,
104 | path: str,
105 | cell_type: Literal["code", "markdown"],
106 | source: Union[str, list[str]]
107 | ) -> int:
108 | """
109 | Append a cell to notebook.
110 |
111 | Args:
112 | path: Notebook path
113 | cell_type: Type of cell
114 | source: Cell source code/markdown
115 |
116 | Returns:
117 | Index of appended cell
118 | """
119 | pass
120 |
121 | @abstractmethod
122 | async def insert_cell(
123 | self,
124 | path: str,
125 | cell_index: int,
126 | cell_type: Literal["code", "markdown"],
127 | source: Union[str, list[str]]
128 | ) -> int:
129 | """
130 | Insert a cell at specific index.
131 |
132 | Args:
133 | path: Notebook path
134 | cell_index: Where to insert
135 | cell_type: Type of cell
136 | source: Cell source
137 |
138 | Returns:
139 | Index of inserted cell
140 | """
141 | pass
142 |
143 | @abstractmethod
144 | async def delete_cell(self, path: str, cell_index: int) -> None:
145 | """
146 | Delete a cell from notebook.
147 |
148 | Args:
149 | path: Notebook path
150 | cell_index: Index of cell to delete
151 | """
152 | pass
153 |
154 | @abstractmethod
155 | async def overwrite_cell(
156 | self,
157 | path: str,
158 | cell_index: int,
159 | new_source: Union[str, list[str]]
160 | ) -> tuple[str, str]:
161 | """
162 | Overwrite cell content.
163 |
164 | Args:
165 | path: Notebook path
166 | cell_index: Index of cell to overwrite
167 | new_source: New source content
168 |
169 | Returns:
170 | Tuple of (old_source, new_source) for diff generation
171 | """
172 | pass
173 |
174 | # Kernel operations
175 |
176 | @abstractmethod
177 | async def get_or_create_kernel(self, path: str, kernel_id: Optional[str] = None) -> str:
178 | """
179 | Get existing kernel or create new one for a notebook.
180 |
181 | Args:
182 | path: Notebook path
183 | kernel_id: Specific kernel ID (None to create new)
184 |
185 | Returns:
186 | Kernel ID
187 | """
188 | pass
189 |
190 | @abstractmethod
191 | async def execute_cell(
192 | self,
193 | path: str,
194 | cell_index: int,
195 | kernel_id: str,
196 | timeout_seconds: int = 300
197 | ) -> list[Union[str, ImageContent]]:
198 | """
199 | Execute a cell and return outputs.
200 |
201 | Args:
202 | path: Notebook path
203 | cell_index: Index of cell to execute
204 | kernel_id: Kernel to use
205 | timeout_seconds: Execution timeout
206 |
207 | Returns:
208 | List of cell outputs
209 | """
210 | pass
211 |
212 | @abstractmethod
213 | async def interrupt_kernel(self, kernel_id: str) -> None:
214 | """
215 | Interrupt a running kernel.
216 |
217 | Args:
218 | kernel_id: Kernel to interrupt
219 | """
220 | pass
221 |
222 | @abstractmethod
223 | async def restart_kernel(self, kernel_id: str) -> None:
224 | """
225 | Restart a kernel.
226 |
227 | Args:
228 | kernel_id: Kernel to restart
229 | """
230 | pass
231 |
232 | @abstractmethod
233 | async def shutdown_kernel(self, kernel_id: str) -> None:
234 | """
235 | Shutdown a kernel.
236 |
237 | Args:
238 | kernel_id: Kernel to shutdown
239 | """
240 | pass
241 |
242 | @abstractmethod
243 | async def list_kernels(self) -> list[dict[str, Any]]:
244 | """
245 | List all running kernels.
246 |
247 | Returns:
248 | List of kernel information dictionaries
249 | """
250 | pass
251 |
252 | @abstractmethod
253 | async def kernel_exists(self, kernel_id: str) -> bool:
254 | """
255 | Check if a kernel exists.
256 |
257 | Args:
258 | kernel_id: Kernel ID to check
259 |
260 | Returns:
261 | True if kernel exists
262 | """
263 | pass
264 |
--------------------------------------------------------------------------------
/docs/static/img/feature_1.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
183 |
--------------------------------------------------------------------------------