This is page 4 of 5. Use http://codebase.md/datalayer/jupyter-mcp-server?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .github │ ├── copilot-instructions.md │ ├── dependabot.yml │ └── workflows │ ├── build.yml │ ├── fix-license-header.yml │ ├── lint.sh │ ├── release.yml │ └── test.yml ├── .gitignore ├── .licenserc.yaml ├── .pre-commit-config.yaml ├── .vscode │ ├── mcp.json │ └── settings.json ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── dev │ ├── content │ │ ├── new.ipynb │ │ ├── notebook.ipynb │ │ └── README.md │ └── README.md ├── Dockerfile ├── docs │ ├── .gitignore │ ├── .yarnrc.yml │ ├── babel.config.js │ ├── docs │ │ ├── _category_.yaml │ │ ├── clients │ │ │ ├── _category_.yaml │ │ │ ├── claude_desktop │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── cline │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── cursor │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ ├── vscode │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ └── windsurf │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── configure │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── contribute │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── deployment │ │ │ ├── _category_.yaml │ │ │ ├── datalayer │ │ │ │ ├── _category_.yaml │ │ │ │ └── streamable-http │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ └── jupyter │ │ │ ├── _category_.yaml │ │ │ ├── index.mdx │ │ │ ├── stdio │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ └── streamable-http │ │ │ ├── _category_.yaml │ │ │ ├── jupyter-extension │ │ │ │ └── index.mdx │ │ │ └── standalone │ │ │ └── index.mdx │ │ ├── index.mdx │ │ ├── releases │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── resources │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ └── tools │ │ ├── _category_.yaml │ │ └── index.mdx │ ├── docusaurus.config.js │ ├── LICENSE │ ├── Makefile │ ├── package.json │ ├── README.md │ ├── sidebars.js │ ├── src │ │ ├── components │ │ │ ├── HomepageFeatures.js │ │ │ ├── HomepageFeatures.module.css │ │ │ ├── HomepageProducts.js │ │ │ └── HomepageProducts.module.css │ │ ├── css │ │ │ └── custom.css │ │ ├── pages │ │ │ ├── index.module.css │ │ │ ├── markdown-page.md │ │ │ └── testimonials.tsx │ │ └── theme │ │ └── CustomDocItem.tsx │ └── static │ └── img │ ├── datalayer │ │ ├── logo.png │ │ └── logo.svg │ ├── favicon.ico │ ├── feature_1.svg │ ├── feature_2.svg │ ├── feature_3.svg │ ├── product_1.svg │ ├── product_2.svg │ └── product_3.svg ├── examples │ └── integration_example.py ├── jupyter_mcp_server │ ├── __init__.py │ ├── __main__.py │ ├── __version__.py │ ├── config.py │ ├── enroll.py │ ├── env.py │ ├── jupyter_extension │ │ ├── __init__.py │ │ ├── backends │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── local_backend.py │ │ │ └── remote_backend.py │ │ ├── context.py │ │ ├── extension.py │ │ ├── handlers.py │ │ └── protocol │ │ ├── __init__.py │ │ └── messages.py │ ├── models.py │ ├── notebook_manager.py │ ├── server_modes.py │ ├── server.py │ ├── tools │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _registry.py │ │ ├── assign_kernel_to_notebook_tool.py │ │ ├── delete_cell_tool.py │ │ ├── execute_cell_tool.py │ │ ├── execute_ipython_tool.py │ │ ├── insert_cell_tool.py │ │ ├── insert_execute_code_cell_tool.py │ │ ├── list_cells_tool.py │ │ ├── list_files_tool.py │ │ ├── list_kernels_tool.py │ │ ├── list_notebooks_tool.py │ │ ├── overwrite_cell_source_tool.py │ │ ├── read_cell_tool.py │ │ ├── read_cells_tool.py │ │ ├── restart_notebook_tool.py │ │ ├── unuse_notebook_tool.py │ │ └── use_notebook_tool.py │ └── utils.py ├── jupyter-config │ ├── jupyter_notebook_config │ │ └── jupyter_mcp_server.json │ └── jupyter_server_config.d │ └── jupyter_mcp_server.json ├── LICENSE ├── Makefile ├── pyproject.toml ├── pytest.ini ├── README.md ├── RELEASE.md ├── smithery.yaml └── tests ├── __init__.py ├── conftest.py ├── test_common.py ├── test_config.py ├── test_jupyter_extension.py ├── test_list_kernels.py ├── test_tools.py └── test_use_notebook.py ``` # Files -------------------------------------------------------------------------------- /docs/static/img/product_2.svg: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- ~ Copyright (c) 2023-2024 Datalayer, Inc. ~ ~ BSD 3-Clause License --> <svg xmlns:figma="http://www.figma.com/figma/ns" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="0 0 335.67566 308.79167" version="1.1" id="svg1327" sodipodi:docname="2.svg" inkscape:version="1.0.1 (c497b03c, 2020-09-10)" width="335.67566" height="308.79166"> <metadata id="metadata1331"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title>Web_hosting_SVG</dc:title> </cc:Work> </rdf:RDF> </metadata> <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1357" inkscape:window-height="701" id="namedview1329" showgrid="false" inkscape:zoom="0.98691435" inkscape:cx="142.90769" inkscape:cy="115.1505" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="svg1327" inkscape:document-rotation="0" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" /> <defs id="defs835"> <style id="style833">.cls-1,.cls-7{fill:#d6d8e5;}.cls-1{opacity:0.4;}.cls-2{fill:#d5d6e0;}.cls-3{fill:#e9eaf4;}.cls-4{fill:#dfe0ea;}.cls-5{fill:#2b303f;}.cls-6{fill:#8c50ff;}.cls-8{fill:#edf0f9;}.cls-9{fill:#e2e5f2;}.cls-10{fill:#e5e5e5;}.cls-11{fill:#f4f4f4;}.cls-12{fill:#bfbfbf;}.cls-13{fill:#dceeff;}.cls-14{fill:#dbdbdb;}.cls-15{fill:#1e212d;}.cls-16{fill:#ffcea9;}.cls-17{fill:#ededed;}.cls-18{fill:#38226d;}.cls-19{fill:#9c73ff;}.cls-20{fill:#3a2c6d;}</style> </defs> <title id="title837">Web_hosting_SVG</title> <polygon class="cls-1" points="249.36,272.19 251.14,273.21 107.37,356.22 106.48,354.68 " id="polygon839" transform="translate(0.00186273,-119.51243)" /> <path class="cls-1" d="m 49.311863,306.76757 -45.8000003,-26.45 c -5.2,-3 -4.55,-8.23 1.45,-11.69 l 23.2500003,-13.43 c 6,-3.46 15.07,-3.84 20.26,-0.84 l 45.8,26.45 c 5.2,3 4.54,8.23 -1.45,11.69 l -23.25,13.43 c -6,3.46 -15.07,3.84 -20.26,0.84 z" id="path1155" /> <path class="cls-7" d="m 118.10186,273.76757 v -63.94 l -97.749997,-11 v 64.34 0 c -0.07,2 1.06,3.84 3.48,5.24 l 45.8,26.44 c 5.19,3 14.27,2.62 20.26,-0.84 l 23.249997,-13.42 c 3.38,-2 5.06,-4.46 5,-6.82 z" id="path1157" /> <path class="cls-8" d="m 118.08186,216.80757 v -7 h -7.52 l -41.759997,-24.1 c -5.19,-3 -14.26,-2.62 -20.26,0.84 l -23.25,13.42 a 13.81,13.81 0 0 0 -1.2,0.78 l -3.74,-1.92 c 0,0 0,7.37 0,7.62 v 0 c -0.05,2 1.08,3.83 3.49,5.22 l 45.8,26.44 c 5.19,3 14.27,2.63 20.26,-0.84 l 23.249997,-13.42 c 3.48,-2 5.15,-4.61 4.93,-7 z" id="path1159" /> <path class="cls-9" d="m 69.631863,230.48757 -45.8,-26.44 c -5.19,-3 -4.54,-8.23 1.46,-11.7 l 23.25,-13.42 c 6,-3.46 15.07,-3.84 20.26,-0.84 l 45.799997,26.44 c 5.19,3 4.54,8.24 -1.46,11.7 l -23.249997,13.44 c -5.99,3.47 -15.07,3.82 -20.26,0.82 z" id="path1161" /> <g id="_Группа_" data-name="<Группа>" transform="translate(0.00186273,-119.51243)"> <path id="_Контур_" data-name="<Контур>" class="cls-10" d="m 93.16,326 v 0 A 2.25,2.25 0 0 1 92,328 l -21.1,12.18 a 2.26,2.26 0 0 1 -2.25,0 L 39.13,323.07 A 2.24,2.24 0 0 1 38,321.12 V 321 Z" /> <path id="_Контур_2" data-name="<Контур>" class="cls-10" d="m 92,328 -21.1,12.18 a 2.24,2.24 0 0 1 -1.12,0.3 V 326 H 93.16 A 2.25,2.25 0 0 1 92,328 Z" /> <g id="_Группа_2" data-name="<Группа>"> <path id="_Контур_3" data-name="<Контур>" class="cls-11" d="M 70.61,339 A 1.59,1.59 0 0 1 69,339 L 38.3,321.29 a 0.43,0.43 0 0 1 0,-0.82 L 60.93,307.4 93.16,326 Z" /> </g> <g id="_Группа_3" data-name="<Группа>"> <polygon id="_Контур_4" data-name="<Контур>" class="cls-12" points="50.92,326.01 51.08,326.1 60.93,331.79 66.33,328.67 66.48,328.58 56.48,322.8 " /> <polygon id="_Контур_5" data-name="<Контур>" class="cls-10" points="66.33,328.67 56.48,322.98 51.08,326.1 60.93,331.79 " /> </g> <path class="cls-5" d="M 84.61,327.13 86,328 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.07,0.08 -0.08,0.14 0.03,0.21 z" id="path1170" /> <path class="cls-5" d="m 82.6,326 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.05 -0.12,0.16 -0.01,0.26 z" id="path1172" /> <path class="cls-5" d="m 80.59,324.81 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.08 -0.12,0.19 -0.01,0.26 z" id="path1174" /> <path class="cls-5" d="m 78.58,323.65 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.07 -0.12,0.19 -0.01,0.26 z" id="path1176" /> <path class="cls-5" d="m 76.57,322.49 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.79 c -0.1,0.06 -0.11,0.18 0,0.25 z" id="path1178" /> <path class="cls-5" d="m 74.56,321.33 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.27 l -1.42,-0.82 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.1,0.07 -0.11,0.19 0,0.26 z" id="path1180" /> <path class="cls-5" d="M 72.55,320.17 74,321 a 0.55,0.55 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.78 c -0.13,0.09 -0.15,0.19 -0.03,0.26 z" id="path1182" /> <path class="cls-5" d="m 70.54,319 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.09 -0.12,0.21 0,0.27 z" id="path1184" /> <path class="cls-5" d="m 68.53,317.84 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.09 -0.12,0.21 0,0.27 z" id="path1186" /> <path class="cls-5" d="m 66.52,316.68 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.14,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.2 0,0.26 z" id="path1188" /> <path class="cls-5" d="m 64.51,315.52 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.14,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.2 0,0.26 z" id="path1190" /> <path class="cls-5" d="m 62.5,314.36 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.09,0.08 -0.11,0.19 0.01,0.26 z" id="path1192" /> <path class="cls-5" d="m 60.49,313.2 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.09,0.06 -0.11,0.19 0.01,0.26 z" id="path1194" /> <path class="cls-5" d="m 57.51,311.48 2.39,1.38 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 l -2.39,-1.38 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.09,0.08 -0.11,0.19 0.01,0.26 z" id="path1196" /> <path class="cls-5" d="m 79.68,326.53 1.42,0.82 a 0.53,0.53 0 0 0 0.46,0 l 1.35,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.09 -0.12,0.21 -0.01,0.27 z" id="path1198" /> <path class="cls-5" d="m 77.67,325.37 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.19 0,0.26 z" id="path1200" /> <path class="cls-5" d="m 75.65,324.21 1.43,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 l -1.43,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.1,0.05 -0.11,0.19 0,0.26 z" id="path1202" /> <path class="cls-5" d="m 73.64,323.05 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 L 75.48,322 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.13,0.08 -0.15,0.22 -0.03,0.27 z" id="path1204" /> <path class="cls-5" d="m 71.64,321.89 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.13,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.08 -0.12,0.2 -0.01,0.26 z" id="path1206" /> <path class="cls-5" d="m 69.63,320.73 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.14,-0.2 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.19 0,0.26 z" id="path1208" /> <path class="cls-5" d="m 67.61,319.57 1.42,0.82 a 0.57,0.57 0 0 0 0.46,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.27 l -1.42,-0.82 a 0.55,0.55 0 0 0 -0.45,0 l -1.35,0.79 c -0.11,0.07 -0.12,0.19 -0.01,0.26 z" id="path1210" /> <path class="cls-5" d="m 65.6,318.4 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.2 0,0.26 z" id="path1212" /> <path class="cls-5" d="m 63.59,317.24 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.1,0.08 -0.11,0.19 0.01,0.26 z" id="path1214" /> <path class="cls-5" d="m 61.57,316.08 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 L 63.41,315 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.79 c -0.13,0.1 -0.14,0.21 -0.03,0.29 z" id="path1216" /> <path class="cls-5" d="m 59.56,314.91 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.14,-0.19 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.09 -0.12,0.21 0,0.27 z" id="path1218" /> <path class="cls-5" d="m 57.54,313.75 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.08 -0.12,0.2 -0.01,0.26 z" id="path1220" /> <path class="cls-5" d="m 81.7,327.7 2.39,1.38 a 0.55,0.55 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.27 l -2.39,-1.38 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.09,0.07 -0.11,0.19 0.01,0.26 z" id="path1222" /> <path class="cls-5" d="m 57.38,311.54 1.42,0.82 c 0.11,0.06 0.11,0.17 0,0.24 l -1.44,0.83 a 0.48,0.48 0 0 1 -0.42,0 l -0.11,-0.07 a 0.45,0.45 0 0 0 -0.39,0 l -1.51,0.88 a 0.45,0.45 0 0 1 -0.42,0 l -0.91,-0.52 a 0.14,0.14 0 0 1 0,-0.25 l 1.42,-0.81 0.29,-0.17 0.21,-0.13 0.22,-0.12 1.22,-0.7 a 0.45,0.45 0 0 1 0.42,0 z" id="path1224" /> <path class="cls-5" d="m 76.81,331.63 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.27 l -1.42,-0.82 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.1,0.07 -0.11,0.19 0,0.26 z" id="path1226" /> <path class="cls-5" d="m 74.81,330.47 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.1,0.08 -0.11,0.19 0.01,0.26 z" id="path1228" /> <path class="cls-5" d="m 72.8,329.31 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.11,0.2 0,0.26 z" id="path1230" /> <path class="cls-5" d="m 55.73,319.45 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.35,-0.79 c 0.14,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.09,0.09 -0.11,0.21 0.01,0.27 z" id="path1232" /> <path class="cls-5" d="m 53.72,318.3 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.27 l -1.42,-0.82 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.1,0.07 -0.11,0.19 0,0.26 z" id="path1234" /> <path class="cls-5" d="m 51.72,317.14 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.09,0.12 -0.11,0.19 0.01,0.26 z" id="path1236" /> <path class="cls-5" d="m 49.71,316 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.06 -0.11,0.17 0,0.26 z" id="path1238" /> <path class="cls-5" d="m 70.28,327.86 1.94,1.14 a 0.51,0.51 0 0 0 0.45,0 l 1.35,-0.79 c 0.14,-0.07 0.15,-0.19 0,-0.26 l -1.94,-1.12 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.09,0.05 -0.11,0.17 0.01,0.24 z" id="path1240" /> <path class="cls-5" d="m 57.73,320.61 1.94,1.12 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.14,-0.2 0,-0.26 l -1.94,-1.12 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.11,0.19 0,0.26 z" id="path1242" /> <path class="cls-5" d="m 60.25,322.07 9.45,5.45 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 L 62.09,321 a 0.51,0.51 0 0 0 -0.45,0 l -1.35,0.79 c -0.14,0.09 -0.15,0.21 -0.04,0.28 z" id="path1244" /> <path class="cls-5" d="m 77.19,327.34 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.07 0.14,-0.19 0,-0.26 L 79,326.28 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.1 -0.12,0.22 0,0.28 z" id="path1246" /> <path class="cls-5" d="M 75.18,326.18 76.6,327 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 L 77,325.12 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.11,0.1 -0.13,0.22 -0.01,0.28 z" id="path1248" /> <path class="cls-5" d="m 73.17,325 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 L 76.4,325 c 0.13,-0.07 0.15,-0.19 0,-0.26 L 75,324 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.12,0.06 -0.14,0.22 -0.02,0.22 z" id="path1250" /> <path class="cls-5" d="m 71.16,323.86 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 L 73,322.8 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.13,0.1 -0.14,0.22 -0.03,0.28 z" id="path1252" /> <path class="cls-5" d="m 69.15,322.71 1.42,0.82 a 0.57,0.57 0 0 0 0.46,0 l 1.35,-0.79 c 0.14,-0.07 0.15,-0.19 0,-0.26 L 71,321.64 a 0.51,0.51 0 0 0 -0.45,0 l -1.35,0.78 c -0.15,0.1 -0.2,0.22 -0.05,0.29 z" id="path1254" /> <path class="cls-5" d="m 67.15,321.55 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.35,-0.79 c 0.14,-0.07 0.15,-0.19 0,-0.26 L 69,320.48 a 0.57,0.57 0 0 0 -0.46,0 l -1.35,0.78 c -0.19,0.1 -0.19,0.22 -0.04,0.29 z" id="path1256" /> <path class="cls-5" d="m 65.14,320.39 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.27 L 67,319.32 a 0.57,0.57 0 0 0 -0.46,0 l -1.35,0.79 C 65,320.2 65,320.32 65.14,320.39 Z" id="path1258" /> <path class="cls-5" d="m 63.13,319.23 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.14,-0.2 0,-0.27 L 65,318.16 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 C 63,319 63,319.16 63.13,319.23 Z" id="path1260" /> <path class="cls-5" d="m 61.12,318.07 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.27 L 63,317 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 C 61,317.88 61,318 61.12,318.07 Z" id="path1262" /> <path class="cls-5" d="m 59.11,316.91 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 L 61,315.85 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.79 c -0.19,0.08 -0.19,0.2 -0.08,0.27 z" id="path1264" /> <path class="cls-5" d="m 57.1,315.75 1.42,0.82 a 0.53,0.53 0 0 0 0.46,0 l 1.35,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.35,0.79 C 57,315.56 57,315.68 57.1,315.75 Z" id="path1266" /> <path class="cls-5" d="m 55.09,314.59 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.35,0.79 c -0.1,0.06 -0.1,0.18 -0.01,0.25 z" id="path1268" /> <path class="cls-5" d="m 79.2,328.5 2.93,1.7 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 L 81,327.44 a 0.5,0.5 0 0 0 -0.46,0 l -1.35,0.78 c -0.1,0.1 -0.11,0.22 0.01,0.28 z" id="path1270" /> <path class="cls-5" d="m 76.24,329 1.42,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.35,-0.79 c 0.14,-0.07 0.15,-0.19 0,-0.26 L 78.08,328 a 0.53,0.53 0 0 0 -0.46,0 l -1.35,0.78 c -0.14,0.08 -0.15,0.22 -0.03,0.22 z" id="path1272" /> <path class="cls-5" d="m 74.23,327.89 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.14,-0.19 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.2 0,0.27 z" id="path1274" /> <path class="cls-5" d="m 72.22,326.73 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.36,-0.79 c 0.13,-0.07 0.15,-0.19 0,-0.26 l -1.42,-0.82 a 0.51,0.51 0 0 0 -0.45,0 l -1.36,0.78 c -0.1,0.08 -0.12,0.2 0,0.27 z" id="path1276" /> <path class="cls-5" d="m 70.21,325.57 1.42,0.82 a 0.55,0.55 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.27 l -1.42,-0.82 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.1,0.07 -0.11,0.19 0,0.26 z" id="path1278" /> <path class="cls-5" d="m 68.2,324.41 1.42,0.82 a 0.53,0.53 0 0 0 0.46,0 l 1.35,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.27 L 70,323.34 a 0.55,0.55 0 0 0 -0.45,0 l -1.35,0.79 c -0.1,0.09 -0.11,0.21 0,0.28 z" id="path1280" /> <path class="cls-5" d="m 66.19,323.25 1.43,0.82 a 0.51,0.51 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 L 68,322.19 a 0.51,0.51 0 0 0 -0.45,0 l -1.35,0.79 c -0.11,0.08 -0.12,0.2 -0.01,0.27 z" id="path1282" /> <path class="cls-5" d="m 64.19,322.09 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.2 0,-0.26 L 66,321 a 0.53,0.53 0 0 0 -0.46,0 l -1.35,0.79 c -0.1,0.11 -0.12,0.21 0,0.3 z" id="path1284" /> <path class="cls-5" d="m 62.18,320.93 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.14,-0.2 0,-0.26 L 64,319.87 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.11,0.1 -0.13,0.21 -0.01,0.28 z" id="path1286" /> <path class="cls-5" d="m 60.17,319.77 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 L 62,318.71 a 0.48,0.48 0 0 0 -0.45,0 l -1.36,0.78 c -0.12,0.1 -0.14,0.21 -0.02,0.28 z" id="path1288" /> <path class="cls-5" d="m 58.16,318.61 1.42,0.82 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.2 0,-0.26 L 60,317.55 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.14,0.1 -0.15,0.21 -0.04,0.28 z" id="path1290" /> <path class="cls-5" d="m 56.15,317.45 1.42,0.82 a 0.5,0.5 0 0 0 0.46,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 L 58,316.39 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.15,0.1 -0.2,0.21 -0.05,0.28 z" id="path1292" /> <path class="cls-5" d="m 51.66,314.86 3.91,2.25 a 0.48,0.48 0 0 0 0.45,0 l 1.35,-0.78 c 0.14,-0.08 0.15,-0.19 0,-0.26 l -3.91,-2.26 a 0.55,0.55 0 0 0 -0.45,0 l -1.36,0.79 c -0.09,0.07 -0.11,0.19 0.01,0.26 z" id="path1294" /> <path class="cls-5" d="m 78.24,330.2 1.94,1.12 a 0.48,0.48 0 0 0 0.45,0 l 1.36,-0.78 c 0.13,-0.08 0.15,-0.19 0,-0.26 l -1.94,-1.12 a 0.48,0.48 0 0 0 -0.45,0 l -1.35,0.78 c -0.11,0.06 -0.12,0.2 -0.01,0.26 z" id="path1296" /> </g> <g id="_Группа_4" data-name="<Группа>" transform="translate(0.00186273,-119.51243)"> <g id="_Группа_5" data-name="<Группа>"> <path id="_Контур_6" data-name="<Контур>" class="cls-10" d="m 93.16,326 v 0 L 60.93,307.4 v -25.35 l 0.25,-0.56 0.26,-0.14 v 0 a 2.27,2.27 0 0 1 2.23,0 l 29.49,17 a 2.22,2.22 0 0 1 1.11,1.93 v 23.78 c -0.2,1.26 -0.42,1.55 -1.11,1.94 z" /> <path id="_Контур_7" data-name="<Контур>" class="cls-10" d="M 90.2,301.45 60.93,282.9 v -0.85 l 0.25,-0.56 0.26,-0.14 a 2.27,2.27 0 0 1 2.23,0 l 29.49,17 a 2.25,2.25 0 0 1 0.85,0.88 z" /> <path id="_Контур_8" data-name="<Контур>" class="cls-13" d="m 61.67,281.48 31,17.87 a 1.08,1.08 0 0 1 0.54,0.94 V 326 L 60.93,307.4 v -25.49 a 0.49,0.49 0 0 1 0.74,-0.43 z" /> <path id="_Контур_9" data-name="<Контур>" class="cls-11" d="m 61.67,281.48 31,17.87 a 1.08,1.08 0 0 1 0.54,0.94 V 326 L 60.93,307.4 v -25.49 a 0.49,0.49 0 0 1 0.74,-0.43 z" /> <polygon id="_Контур_10" data-name="<Контур>" class="cls-14" points="60.93,306.48 60.93,307.4 93.16,326.01 93.16,325.09 " /> </g> <polygon id="_Контур_11" data-name="<Контур>" class="cls-15" points="92.12,323.11 92.12,300.34 62.16,283.04 62.16,305.86 " /> </g> <g id="Men_2" transform="translate(0.00186273,-119.51243)"> <path id="_Контур_12" data-name="<Контур>" class="cls-16" d="m 8.66,303.64 c -0.27,2.4 -3.13,12.72 -2.51,16.25 0.62,3.53 10.36,9.45 10.36,9.45 l 1.35,-6 -5.4,-5.34 1.46,-10 z" /> <path id="_Контур_13" data-name="<Контур>" class="cls-17" d="m 16.21,293.47 a 4.27,4.27 0 0 0 -5.43,1.64 c -1.44,2.38 -2.93,10.53 -3.06,11.89 0,0 2,2.78 5,2.32 z" /> <path class="cls-18" d="m 27.34,408.56 a 8.36,8.36 0 0 0 6.31,-0.77 c 1.52,0 9.78,-3.48 10.9,-0.52 1,2.6 -2.59,5 -4.5,5.47 -4.17,1 -7.25,3.09 -8.93,3.44 -1.25,0.26 -2.86,0.43 -3.85,-0.55 -1.2,-1.17 -1.09,-5.52 0.07,-7.07 z" id="path1309" /> <path class="cls-19" d="m 31.12,415.46 c 1.68,-0.35 4.76,-2.43 8.93,-3.44 1.6,-0.39 4.35,-2.14 4.63,-4.23 0.37,2.4 -2.85,4.52 -4.63,4.95 -4.17,1 -7.25,3.09 -8.93,3.44 -1.25,0.26 -2.86,0.43 -3.85,-0.55 a 2.34,2.34 0 0 1 -0.55,-1 5,5 0 0 0 4.4,0.83 z" id="path1311" /> <path class="cls-18" d="m 14,402.55 a 8.38,8.38 0 0 0 6.31,-0.77 c 1.52,0.05 9.77,-3.48 10.89,-0.52 1,2.6 -2.59,5 -4.5,5.47 -4.16,1 -7.25,3.09 -8.92,3.44 -1.25,0.26 -2.86,0.43 -3.86,-0.55 -1.23,-1.17 -1.13,-5.52 0.08,-7.07 z" id="path1313" /> <path class="cls-19" d="m 17.74,409.45 c 1.67,-0.35 4.76,-2.43 8.92,-3.44 1.6,-0.39 4.36,-2.14 4.64,-4.23 0.37,2.4 -2.85,4.52 -4.64,5 -4.16,1 -7.25,3.09 -8.92,3.44 -1.25,0.26 -2.86,0.43 -3.86,-0.55 a 2.33,2.33 0 0 1 -0.54,-1 5,5 0 0 0 4.4,0.78 z" id="path1315" /> <path id="_Контур_14" data-name="<Контур>" class="cls-6" d="m 13.8,380.13 a 49.34,49.34 0 0 1 1.9,-9 c 0,0 -0.57,-7.8 -0.85,-15.2 -0.33,-8.39 -3.24,-15.77 -0.94,-22.47 l 25.89,5.76 c 0,0 -1.56,33.45 -1.91,37.5 a 76.61,76.61 0 0 1 -1.16,9.4 c -1.18,6.49 -3.1,21.94 -3.1,21.94 -2.74,1.59 -6.34,0.71 -6.34,0.71 0,0 0.2,-19.54 0.38,-24.22 0.21,-5.71 0.39,-5.27 0.39,-5.27 L 27.2,365 26.64,357.92 c 0,0 -0.72,5.2 -1.18,9.84 -0.41,4.05 -1.39,7.71 -2.48,15.24 -0.94,6.54 -2.48,19.67 -2.48,19.67 -2.74,1.59 -6.46,0.27 -6.46,0.27 0,0 -0.98,-17.14 -0.24,-22.81 z" /> <path id="_Контур_15" data-name="<Контур>" class="cls-16" d="m 17.58,293.61 c 2,0.1 3.44,0.61 3.68,0 a 24,24 0 0 0 0.45,-3.08 c -0.22,-0.57 -0.42,-1.18 -0.42,-1.18 -2.48,-1.53 -3.26,-4.2 -3.58,-7.32 -0.56,-5.33 2.32,-10 7.65,-10.55 5,-0.52 8.69,3 9.67,7.83 0.53,2.39 1.58,6.9 0.16,10.82 -0.8,2.2 -1.82,3.94 -2.85,4.28 a 26.44,26.44 0 0 1 -3,-0.26 v 0 c 0,0 -0.27,1.48 -0.45,2.43 -0.18,0.95 -0.11,1.45 1.78,2.61 1.89,1.16 -2.86,3.56 -6,3.33 -3.14,-0.23 -6.6,-2.11 -7.55,-4.12 -0.99,-2.22 -0.9,-4.85 0.46,-4.79 z" /> <path id="_Контур_16" data-name="<Контур>" class="cls-11" d="m 20.43,293.9 c -0.55,1.37 0.84,3 4.55,4.2 3.71,1.2 4,-0.34 4,-0.34 a 74.4,74.4 0 0 1 7.2,3.71 c 2.22,1.56 3.21,6 3.51,13.64 0.34,8.79 0.37,21.82 0.08,24.07 0,0 -4.15,4.14 -9.47,3.7 -5.32,-0.44 -14.26,-5.14 -16.43,-8.77 0.06,-7.7 1,-9 -0.28,-13.4 -2.86,-10.15 -4.36,-14.31 -3,-21.06 1.14,-5.57 2.74,-6.26 4.94,-6.2 a 45.35,45.35 0 0 1 4.9,0.45 z" /> <path id="_Контур_17" data-name="<Контур>" class="cls-16" d="m 48.07,322.38 c -4.36,1.1 -5.38,-1.66 -6,-3.93 -1.38,-5.52 -2.13,-10.5 -3.17,-13.63 -1.21,-3.69 -2.49,-4.28 -4.29,-5 -2.1,-0.84 -3.91,0.94 -3.21,5.64 a 94.91,94.91 0 0 0 4,16 c 0.55,1.72 1.77,4.77 3.59,6.25 2.25,1.82 5.64,1.74 10.77,0.46 2.18,-0.54 4.58,-1.85 8.72,-3.78 1.11,-0.52 2,-0.86 4.11,-1.85 a 14.64,14.64 0 0 0 5.47,-4.27 c 1.11,-1.62 1.26,-2.38 1,-2.75 -0.26,-0.37 -0.79,-0.36 -1.38,0.32 a 12.12,12.12 0 0 1 -3.14,2.91 c 0,0 1.37,-1.43 2.12,-2.35 a 12,12 0 0 0 1.65,-2.7 c 0.39,-0.94 -0.42,-2.22 -1,-1.55 -0.58,0.67 -0.92,1.36 -2,2.71 a 11.58,11.58 0 0 1 -2,1.93 22.06,22.06 0 0 0 1.87,-3 5,5 0 0 0 0.68,-2.71 c 0,-0.49 -0.73,-1.08 -1.39,-0.14 a 17.61,17.61 0 0 1 -2.34,3.58 c -1,1 -1.84,1.63 -1.87,1.34 -0.03,-0.29 0.61,-0.86 1,-2.06 0.39,-1.2 0,-2.48 -0.66,-2.61 -0.66,-0.13 -0.54,-0.11 -1.08,1 a 35.32,35.32 0 0 0 -2,3.6 7.42,7.42 0 0 1 -1.79,3.09 c -1.14,1.19 -3.51,2.45 -7.66,3.5 z" /> <path id="_Контур_18" data-name="<Контур>" class="cls-17" d="m 33,299.07 c 2.91,-0.43 5,0.88 6.26,5.29 1.26,4.41 2.18,8 2.18,8 a 7.89,7.89 0 0 1 -4.88,3.24 c -3.48,0.85 -4.74,-0.82 -4.74,-0.82 0,0 -1,-4.62 -1.57,-7.77 -0.57,-3.15 -0.81,-7.41 2.75,-7.94 z" /> <path class="cls-20" d="m 34.86,278.63 c 0,0 4.6,-7 -5.37,-8.43 -7.19,-1 -11.81,3.6 -12.35,8.61 -0.51,4.79 2.44,9.77 4.57,11.74 1,0.37 3.28,0.59 6.22,-0.75 a 35.4,35.4 0 0 0 0.14,-3.92 c 0,0 -3.51,-7.38 6.79,-7.25 z" id="path1322" /> </g> <g id="Canvas" transform="matrix(4.0495342,0,0,4.0495342,-6480.5841,-9936.3011)" figma:type="canvas"> <g id="g2566" style="mix-blend-mode:normal" figma:type="group"> <g id="g2564" style="mix-blend-mode:normal" figma:type="group"> <g id="Group" style="mix-blend-mode:normal" figma:type="group"> <g id="g" style="mix-blend-mode:normal" figma:type="group"> <g id="path" style="mix-blend-mode:normal" figma:type="group"> <g id="path9 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2501" d="m 1642.285,2479.8353 c 0,1.5581 -0.1247,2.0655 -0.4452,2.4394 -0.3567,0.3213 -0.8198,0.4989 -1.2998,0.4986 l 0.1246,0.8903 c 0.7442,0.01 1.4664,-0.2528 2.0299,-0.7389 0.3033,-0.3698 0.5289,-0.7969 0.6635,-1.2558 0.1346,-0.4588 0.1754,-0.9401 0.12,-1.4151 v -5.8938 h -1.193 v 5.4397 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> <g id="g2508" style="mix-blend-mode:normal" figma:type="group"> <g id="path10 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2505" d="m 1651.182,2479.1331 c 0,0.6677 0,1.2642 0.053,1.7806 h -1.0595 l -0.071,-1.0595 c -0.2216,0.3749 -0.5385,0.6844 -0.9185,0.897 -0.38,0.2127 -0.8095,0.321 -1.2449,0.3138 -1.0328,0 -2.2614,-0.5609 -2.2614,-2.8489 v -3.8016 h 1.193 v 3.5612 c 0,1.2375 0.3828,2.0655 1.4601,2.0655 0.2216,0 0.4415,-0.04 0.6467,-0.1232 0.2053,-0.084 0.3917,-0.2076 0.5484,-0.3643 0.1567,-0.1568 0.2806,-0.3432 0.3643,-0.5484 0.084,-0.2053 0.1256,-0.4251 0.1233,-0.6468 v -3.9885 h 1.1929 v 4.7275 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> <g id="g2513" style="mix-blend-mode:normal" figma:type="group"> <g id="path11 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2510" d="m 1653.4434,2476.5326 c 0,-0.8279 0,-1.5046 -0.053,-2.1189 h 1.0684 l 0.053,1.1129 c 0.238,-0.4021 0.5807,-0.732 0.9915,-0.9546 0.4107,-0.2227 0.8742,-0.3297 1.3411,-0.3096 1.5847,0 2.7777,1.3265 2.7777,3.303 0,2.3326 -1.4334,3.49 -2.9825,3.49 -0.3965,0.018 -0.7909,-0.067 -1.1449,-0.2467 -0.3541,-0.1793 -0.6558,-0.447 -0.8761,-0.7772 v 0 3.5612 h -1.1752 v -7.0333 z m 1.1752,1.7361 c 0,0.1616 0.021,0.3225 0.053,0.4808 0.101,0.3953 0.331,0.7456 0.6535,0.9956 0.3225,0.2499 0.7191,0.3852 1.1271,0.3843 1.2553,0 1.9943,-1.0238 1.9943,-2.5106 0,-1.2998 -0.6944,-2.4127 -1.9498,-2.4127 -0.4967,0.041 -0.9616,0.2612 -1.3074,0.6201 -0.3459,0.3589 -0.5489,0.8317 -0.5711,1.3297 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> <g id="g2518" style="mix-blend-mode:normal" figma:type="group"> <g id="path12 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2515" d="m 1661.7476,2474.4078 1.4334,3.8372 c 0.1514,0.4273 0.3116,0.9437 0.4185,1.3265 0.1246,-0.3917 0.2581,-0.8903 0.4184,-1.3532 l 1.2998,-3.8105 h 1.2554 l -1.7806,4.6296 c -0.8903,2.2257 -1.4334,3.3742 -2.2525,4.0686 -0.4125,0.3768 -0.9155,0.6406 -1.4601,0.7657 l -0.2938,-0.9972 c 0.3808,-0.1251 0.7343,-0.3215 1.0417,-0.5787 0.4343,-0.3539 0.779,-0.8054 1.006,-1.3176 0.049,-0.089 0.082,-0.1851 0.098,-0.2849 -0.01,-0.1074 -0.037,-0.2126 -0.08,-0.3116 l -2.4216,-5.9917 h 1.2998 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> <g id="g2523" style="mix-blend-mode:normal" figma:type="group"> <g id="path13 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2520" d="m 1669.7401,2472.54 v 1.8696 h 1.7094 v 0.8903 h -1.7094 v 3.5078 c 0,0.8013 0.2315,1.2642 0.8903,1.2642 0.234,0 0.4675,-0.023 0.6945,-0.08 l 0.053,0.8903 c -0.3404,0.1179 -0.6995,0.1722 -1.0595,0.1602 -0.2384,0.015 -0.4772,-0.022 -0.7,-0.1079 -0.2229,-0.086 -0.4244,-0.2193 -0.5909,-0.3906 -0.3626,-0.4851 -0.5281,-1.0895 -0.463,-1.6916 v -3.5612 h -1.0149 v -0.8903 h 1.0327 v -1.5847 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> <g id="g2528" style="mix-blend-mode:normal" figma:type="group"> <g id="path14 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2525" d="m 1673.6472,2477.869 c -0.024,0.3019 0.017,0.6055 0.1221,0.8898 0.1047,0.2842 0.2698,0.5423 0.484,0.7565 0.2142,0.2142 0.4723,0.3793 0.7566,0.484 0.2842,0.1046 0.5878,0.1463 0.8897,0.1222 0.6107,0.014 1.2175,-0.1017 1.7806,-0.3384 l 0.2048,0.8903 c -0.6911,0.2847 -1.4341,0.4212 -2.1812,0.4007 -0.4356,0.03 -0.8724,-0.035 -1.2806,-0.1898 -0.4081,-0.1549 -0.778,-0.3962 -1.0841,-0.7074 -0.3062,-0.3112 -0.5414,-0.685 -0.6895,-1.0956 -0.1481,-0.4107 -0.2057,-0.8485 -0.1687,-1.2835 0,-1.9587 1.1663,-3.5078 3.0715,-3.5078 2.1367,0 2.6709,1.8696 2.6709,3.0626 0.011,0.1838 0.011,0.3682 0,0.552 h -4.6028 z m 3.4899,-0.8903 c 0.034,-0.238 0.017,-0.4806 -0.05,-0.7115 -0.067,-0.2309 -0.1834,-0.4447 -0.3403,-0.6269 -0.1569,-0.1822 -0.3511,-0.3287 -0.5694,-0.4296 -0.2183,-0.1008 -0.4557,-0.1537 -0.6962,-0.155 -0.4892,0.035 -0.9475,0.2522 -1.2851,0.6079 -0.3376,0.3558 -0.5302,0.8248 -0.54,1.3151 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> <g id="g2533" style="mix-blend-mode:normal" figma:type="group"> <g id="path15 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2530" d="m 1680.0334,2476.4323 c 0,-0.7657 0,-1.4245 -0.053,-2.0299 h 1.0684 v 1.2731 h 0.053 c 0.1121,-0.3929 0.3438,-0.7412 0.6629,-0.9965 0.3191,-0.2552 0.7097,-0.4048 1.1177,-0.4279 0.1123,-0.015 0.226,-0.015 0.3383,0 v 1.1128 c -0.1361,-0.016 -0.2735,-0.016 -0.4096,0 -0.4041,0.016 -0.7887,0.1779 -1.082,0.4565 -0.2933,0.2785 -0.4751,0.6542 -0.5116,1.057 -0.033,0.1822 -0.051,0.3668 -0.053,0.552 v 3.4633 h -1.1752 v -4.4515 z" style="mix-blend-mode:normal;fill:#4e4e4e" /> </g> </g> </g> </g> <g id="g2562" style="mix-blend-mode:normal" figma:type="group"> <g id="g2540" style="mix-blend-mode:normal" figma:type="group"> <g id="path16 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2537" d="m 1679.5106,2456.5257 c 0.037,0.5981 -0.1057,1.1935 -0.4088,1.7105 -0.303,0.5169 -0.7531,0.9319 -1.2929,1.1922 -0.5397,0.2602 -1.1447,0.3539 -1.7379,0.2691 -0.5932,-0.085 -1.1477,-0.3442 -1.593,-0.7453 -0.4452,-0.4011 -0.7609,-0.9256 -0.907,-1.5067 -0.146,-0.5812 -0.1158,-1.1927 0.087,-1.7566 0.2027,-0.5639 0.5686,-1.0547 1.0513,-1.4099 0.4826,-0.3551 1.06,-0.5586 1.6586,-0.5845 0.3926,-0.022 0.7855,0.035 1.1562,0.1654 0.3707,0.1308 0.7119,0.3336 1.0039,0.5967 0.2921,0.2631 0.5293,0.5813 0.6979,0.9364 0.1687,0.3551 0.2654,0.74 0.2848,1.1327 z" style="mix-blend-mode:normal;fill:#767677" /> </g> </g> <g id="g2545" style="mix-blend-mode:normal" figma:type="group"> <g id="path17 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2542" d="m 1661.9062,2491.3924 c -8.0126,0 -15.0549,-2.8757 -18.6962,-7.1224 1.4128,3.8204 3.9622,7.1163 7.3048,9.444 3.3426,2.3278 7.3182,3.5756 11.3914,3.5756 4.0733,0 8.0488,-1.2478 11.3915,-3.5756 3.3426,-2.3277 5.8919,-5.6236 7.3048,-9.444 -3.6324,4.2467 -10.648,7.1224 -18.6963,7.1224 z" style="mix-blend-mode:normal;fill:#f37726" /> </g> </g> <g id="g2550" style="mix-blend-mode:normal" figma:type="group"> <g id="path18 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2547" d="m 1661.9062,2463.7773 c 8.0127,0 15.055,2.8756 18.6963,7.1223 -1.4129,-3.8204 -3.9622,-7.1163 -7.3048,-9.444 -3.3427,-2.3277 -7.3182,-3.5756 -11.3915,-3.5756 -4.0732,0 -8.0488,1.2479 -11.3914,3.5756 -3.3426,2.3277 -5.892,5.6236 -7.3048,9.444 3.6413,-4.2556 10.648,-7.1223 18.6962,-7.1223 z" style="mix-blend-mode:normal;fill:#f37726" /> </g> </g> <g id="g2555" style="mix-blend-mode:normal" figma:type="group"> <g id="path19 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2552" d="m 1650.8758,2499.6566 c 0.047,0.7533 -0.1314,1.5036 -0.5123,2.1553 -0.381,0.6516 -0.9473,1.1751 -1.6268,1.5037 -0.6796,0.3286 -1.4415,0.4475 -2.1889,0.3416 -0.7473,-0.106 -1.4462,-0.4321 -2.0076,-0.9367 -0.5614,-0.5046 -0.9598,-1.1649 -1.1446,-1.8967 -0.1847,-0.7319 -0.1474,-1.5022 0.1072,-2.2128 0.2546,-0.7106 0.715,-1.3293 1.3224,-1.7773 0.6075,-0.448 1.3347,-0.705 2.0887,-0.7383 0.494,-0.026 0.9884,0.045 1.4549,0.2094 0.4665,0.1647 0.8959,0.4197 1.2638,0.7504 0.368,0.3307 0.6671,0.7306 0.8804,1.177 0.2133,0.4464 0.3366,0.9304 0.3628,1.4244 z" style="mix-blend-mode:normal;fill:#9e9e9e" /> </g> </g> <g id="g2560" style="mix-blend-mode:normal" figma:type="group"> <g id="path20 fill" style="mix-blend-mode:normal" figma:type="vector"> <path id="use2557" d="m 1644.1206,2462.8094 c -0.4317,0.012 -0.8574,-0.1041 -1.2234,-0.3334 -0.366,-0.2293 -0.656,-0.5618 -0.8336,-0.9555 -0.1775,-0.3937 -0.2347,-0.8312 -0.1643,-1.2573 0.07,-0.4261 0.2652,-0.822 0.5599,-1.1377 0.2948,-0.3157 0.6763,-0.5372 1.0966,-0.6366 0.4203,-0.099 0.8606,-0.072 1.2656,0.078 0.405,0.1501 0.7566,0.4166 1.0105,0.766 0.2539,0.3494 0.3988,0.7661 0.4165,1.1977 0.017,0.5835 -0.1971,1.1501 -0.5955,1.5768 -0.3984,0.4268 -0.949,0.6791 -1.5323,0.7023 z" style="mix-blend-mode:normal;fill:#616262" /> </g> </g> </g> </g> </g> </g> </svg> ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/server.py: -------------------------------------------------------------------------------- ```python # Copyright (c) 2023-2024 Datalayer, Inc. # # BSD 3-Clause License import logging import click import httpx import uvicorn from typing import Union, Optional from fastapi import Request from jupyter_kernel_client import KernelClient from jupyter_server_api import JupyterServerClient from mcp.server import FastMCP from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.middleware.cors import CORSMiddleware from jupyter_mcp_server.models import DocumentRuntime from jupyter_mcp_server.utils import ( extract_output, safe_extract_outputs, create_kernel, start_kernel, ensure_kernel_alive, execute_cell_with_timeout, execute_cell_with_forced_sync, is_kernel_busy, wait_for_kernel_idle, safe_notebook_operation, list_files_recursively, ) from jupyter_mcp_server.config import get_config, set_config from jupyter_mcp_server.notebook_manager import NotebookManager from jupyter_mcp_server.enroll import auto_enroll_document from jupyter_mcp_server.tools import ( # Tool infrastructure ServerMode, # Notebook Management ListNotebooksTool, UseNotebookTool, RestartNotebookTool, UnuseNotebookTool, # Cell Reading ReadCellsTool, ListCellsTool, ReadCellTool, # Cell Writing InsertCellTool, InsertExecuteCodeCellTool, OverwriteCellSourceTool, DeleteCellTool, # Cell Execution ExecuteCellTool, # Other Tools AssignKernelToNotebookTool, ExecuteIpythonTool, ListFilesTool, ListKernelsTool, ) from typing import Literal, Union from mcp.types import ImageContent ############################################################################### logger = logging.getLogger(__name__) ############################################################################### class FastMCPWithCORS(FastMCP): def streamable_http_app(self) -> Starlette: """Return StreamableHTTP server app with CORS middleware See: https://github.com/modelcontextprotocol/python-sdk/issues/187 """ # Get the original Starlette app app = super().streamable_http_app() # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, should set specific domains allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) return app def sse_app(self, mount_path: str | None = None) -> Starlette: """Return SSE server app with CORS middleware""" # Get the original Starlette app app = super().sse_app(mount_path) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, should set specific domains allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) return app ############################################################################### mcp = FastMCPWithCORS(name="Jupyter MCP Server", json_response=False, stateless_http=True) # Initialize the unified notebook manager notebook_manager = NotebookManager() # Initialize all tool instances (no arguments needed - tools receive dependencies via execute()) # Notebook Management Tools list_notebook_tool = ListNotebooksTool() use_notebook_tool = UseNotebookTool() restart_notebook_tool = RestartNotebookTool() unuse_notebook_tool = UnuseNotebookTool() # Cell Reading Tools read_cells_tool = ReadCellsTool() list_cells_tool = ListCellsTool() read_cell_tool = ReadCellTool() # Cell Writing Tools insert_cell_tool = InsertCellTool() insert_execute_code_cell_tool = InsertExecuteCodeCellTool() overwrite_cell_source_tool = OverwriteCellSourceTool() delete_cell_tool = DeleteCellTool() # Cell Execution Tools execute_cell_tool = ExecuteCellTool() # Other Tools assign_kernel_to_notebook_tool = AssignKernelToNotebookTool() execute_ipython_tool = ExecuteIpythonTool() list_files_tool = ListFilesTool() list_kernel_tool = ListKernelsTool() ############################################################################### class ServerContext: """Singleton to cache server mode and context managers.""" _instance = None _mode = None _contents_manager = None _kernel_manager = None _kernel_spec_manager = None _session_manager = None _server_client = None _kernel_client = None _initialized = False @classmethod def get_instance(cls): if cls._instance is None: cls._instance = cls() return cls._instance @classmethod def reset(cls): """Reset the singleton instance. Use this when config changes.""" if cls._instance is not None: cls._instance._initialized = False cls._instance._mode = None cls._instance._contents_manager = None cls._instance._kernel_manager = None cls._instance._kernel_spec_manager = None cls._instance._session_manager = None cls._instance._server_client = None cls._instance._kernel_client = None def initialize(self): """Initialize context once.""" if self._initialized: return try: from jupyter_mcp_server.jupyter_extension.context import get_server_context context = get_server_context() if context.is_local_document() and context.get_contents_manager() is not None: self._mode = ServerMode.JUPYTER_SERVER self._contents_manager = context.get_contents_manager() self._kernel_manager = context.get_kernel_manager() self._kernel_spec_manager = context.get_kernel_spec_manager() if hasattr(context, 'get_kernel_spec_manager') else None self._session_manager = context.get_session_manager() if hasattr(context, 'get_session_manager') else None else: self._mode = ServerMode.MCP_SERVER # Initialize HTTP clients for MCP_SERVER mode config = get_config() # Validate that runtime_url is set and not None/empty # Note: String "None" values should have been normalized by start_command() runtime_url = config.runtime_url if not runtime_url or runtime_url in ("None", "none", "null", ""): raise ValueError( f"runtime_url is not configured (current value: {repr(runtime_url)}). " "Please check:\n" "1. RUNTIME_URL environment variable is set correctly (not the string 'None')\n" "2. --runtime-url argument is provided when starting the server\n" "3. The MCP client configuration passes runtime_url correctly" ) logger.info(f"Initializing MCP_SERVER mode with runtime_url: {runtime_url}") self._server_client = JupyterServerClient(base_url=runtime_url, token=config.runtime_token) # kernel_client will be created lazily when needed except (ImportError, Exception) as e: # If not in Jupyter context, use MCP_SERVER mode if not isinstance(e, ValueError): self._mode = ServerMode.MCP_SERVER # Initialize HTTP clients for MCP_SERVER mode config = get_config() # Validate that runtime_url is set and not None/empty # Note: String "None" values should have been normalized by start_command() runtime_url = config.runtime_url if not runtime_url or runtime_url in ("None", "none", "null", ""): raise ValueError( f"runtime_url is not configured (current value: {repr(runtime_url)}). " "Please check:\n" "1. RUNTIME_URL environment variable is set correctly (not the string 'None')\n" "2. --runtime-url argument is provided when starting the server\n" "3. The MCP client configuration passes runtime_url correctly" ) logger.info(f"Initializing MCP_SERVER mode with runtime_url: {runtime_url}") self._server_client = JupyterServerClient(base_url=runtime_url, token=config.runtime_token) else: raise self._initialized = True logger.info(f"Server mode initialized: {self._mode}") @property def mode(self): if not self._initialized: self.initialize() return self._mode @property def contents_manager(self): if not self._initialized: self.initialize() return self._contents_manager @property def kernel_manager(self): if not self._initialized: self.initialize() return self._kernel_manager @property def kernel_spec_manager(self): if not self._initialized: self.initialize() return self._kernel_spec_manager @property def session_manager(self): if not self._initialized: self.initialize() return self._session_manager @property def server_client(self): if not self._initialized: self.initialize() return self._server_client @property def kernel_client(self): if not self._initialized: self.initialize() return self._kernel_client # Initialize server context singleton server_context = ServerContext.get_instance() ############################################################################### def __create_kernel() -> KernelClient: """Create a new kernel instance using current configuration.""" config = get_config() return create_kernel(config, logger) def __start_kernel(): """Start the Jupyter kernel with error handling (for backward compatibility).""" config = get_config() start_kernel(notebook_manager, config, logger) async def __auto_enroll_document(): """Wrapper for auto_enroll_document that uses server context.""" await auto_enroll_document( config=get_config(), notebook_manager=notebook_manager, use_notebook_tool=use_notebook_tool, server_context=server_context, ) def __ensure_kernel_alive() -> KernelClient: """Ensure kernel is running, restart if needed.""" current_notebook = notebook_manager.get_current_notebook() or "default" return ensure_kernel_alive(notebook_manager, current_notebook, __create_kernel) async def __execute_cell_with_timeout(notebook, cell_index, kernel, timeout_seconds=300): """Execute a cell with timeout and real-time output sync.""" return await execute_cell_with_timeout(notebook, cell_index, kernel, timeout_seconds, logger) async def __execute_cell_with_forced_sync(notebook, cell_index, kernel, timeout_seconds=300): """Execute cell with forced real-time synchronization.""" return await execute_cell_with_forced_sync(notebook, cell_index, kernel, timeout_seconds, logger) def __is_kernel_busy(kernel): """Check if kernel is currently executing something.""" return is_kernel_busy(kernel) async def __wait_for_kernel_idle(kernel, max_wait_seconds=60): """Wait for kernel to become idle before proceeding.""" return await wait_for_kernel_idle(kernel, logger, max_wait_seconds) async def __safe_notebook_operation(operation_func, max_retries=3): """Safely execute notebook operations with connection recovery.""" return await safe_notebook_operation(operation_func, logger, max_retries) def _list_files_recursively(server_client, current_path="", current_depth=0, files=None, max_depth=3): """Recursively list all files and directories in the Jupyter server.""" return list_files_recursively(server_client, current_path, current_depth, files, max_depth) ############################################################################### # Custom Routes. @mcp.custom_route("/api/connect", ["PUT"]) async def connect(request: Request): """Connect to a document and a runtime from the Jupyter MCP server.""" data = await request.json() # Log the received data for diagnostics # Note: set_config() will automatically normalize string "None" values logger.info( f"Connect endpoint received - runtime_url: {repr(data.get('runtime_url'))}, " f"document_url: {repr(data.get('document_url'))}, " f"provider: {data.get('provider')}" ) document_runtime = DocumentRuntime(**data) # Clean up existing default notebook if any if "default" in notebook_manager: try: notebook_manager.remove_notebook("default") except Exception as e: logger.warning(f"Error stopping existing notebook during connect: {e}") # Update configuration with new values # String "None" values will be automatically normalized by set_config() set_config( provider=document_runtime.provider, runtime_url=document_runtime.runtime_url, runtime_id=document_runtime.runtime_id, runtime_token=document_runtime.runtime_token, document_url=document_runtime.document_url, document_id=document_runtime.document_id, document_token=document_runtime.document_token ) # Reset ServerContext to pick up new configuration ServerContext.reset() try: __start_kernel() return JSONResponse({"success": True}) except Exception as e: logger.error(f"Failed to connect: {e}") return JSONResponse({"success": False, "error": str(e)}, status_code=500) @mcp.custom_route("/api/stop", ["DELETE"]) async def stop(request: Request): try: current_notebook = notebook_manager.get_current_notebook() or "default" if current_notebook in notebook_manager: notebook_manager.remove_notebook(current_notebook) return JSONResponse({"success": True}) except Exception as e: logger.error(f"Error stopping notebook: {e}") return JSONResponse({"success": False, "error": str(e)}, status_code=500) @mcp.custom_route("/api/healthz", ["GET"]) async def health_check(request: Request): """Custom health check endpoint""" kernel_status = "unknown" try: current_notebook = notebook_manager.get_current_notebook() or "default" kernel = notebook_manager.get_kernel(current_notebook) if kernel: kernel_status = "alive" if hasattr(kernel, 'is_alive') and kernel.is_alive() else "dead" else: kernel_status = "not_initialized" except Exception: kernel_status = "error" return JSONResponse( { "success": True, "service": "jupyter-mcp-server", "message": "Jupyter MCP Server is running.", "status": "healthy", "kernel_status": kernel_status, } ) ############################################################################### # Tools. ############################################################################### ############################################################################### # Multi-Notebook Management Tools. @mcp.tool() async def use_notebook( notebook_name: str, notebook_path: Optional[str] = None, mode: Literal["connect", "create"] = "connect", kernel_id: Optional[str] = None, ) -> str: """Use a notebook file (connect to existing or create new, or switch to already-connected notebook). Args: notebook_name: Unique identifier for the notebook notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb"). Optional - if not provided, switches to an already-connected notebook with the given name. mode: "connect" to connect to existing, "create" to create new kernel_id: Specific kernel ID to use (optional, will create new if not provided) Returns: str: Success message with notebook information """ config = get_config() return await __safe_notebook_operation( lambda: use_notebook_tool.execute( mode=server_context.mode, server_client=server_context.server_client, notebook_name=notebook_name, notebook_path=notebook_path, use_mode=mode, kernel_id=kernel_id, ensure_kernel_alive_fn=__ensure_kernel_alive, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, session_manager=server_context.session_manager, notebook_manager=notebook_manager, runtime_url=config.runtime_url if config.runtime_url != "local" else None, runtime_token=config.runtime_token, ) ) @mcp.tool() async def list_notebooks() -> str: """List all notebooks in the Jupyter server (including subdirectories) and show which ones are managed. To interact with a notebook, it has to be "managed". If a notebook is not managed, you can use it with the `use_notebook` tool. Returns: str: TSV formatted table with notebook information including management status """ return await list_notebook_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, ) @mcp.tool() async def restart_notebook(notebook_name: str) -> str: """Restart the kernel for a specific notebook. Args: notebook_name: Notebook identifier to restart Returns: str: Success message """ return await restart_notebook_tool.execute( mode=server_context.mode, notebook_name=notebook_name, notebook_manager=notebook_manager, kernel_manager=server_context.kernel_manager, ) @mcp.tool() async def unuse_notebook(notebook_name: str) -> str: """Unuse from a specific notebook and release its resources. Args: notebook_name: Notebook identifier to disconnect Returns: str: Success message """ return await unuse_notebook_tool.execute( mode=server_context.mode, notebook_name=notebook_name, notebook_manager=notebook_manager, kernel_manager=server_context.kernel_manager, ) ############################################################################### # Cell Tools. @mcp.tool() async def insert_cell( cell_index: int, cell_type: Literal["code", "markdown"], cell_source: str, ) -> str: """Insert a cell to specified position. Args: cell_index: target index for insertion (0-based). Use -1 to append at end. cell_type: Type of cell to insert ("code" or "markdown") cell_source: Source content for the cell Returns: str: Success message and the structure of its surrounding cells (up to 5 cells above and 5 cells below) """ return await __safe_notebook_operation( lambda: insert_cell_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, cell_index=cell_index, cell_source=cell_source, cell_type=cell_type, ) ) @mcp.tool() async def insert_execute_code_cell(cell_index: int, cell_source: str) -> list[Union[str, ImageContent]]: """Insert and execute a code cell in a Jupyter notebook. Args: cell_index: Index of the cell to insert (0-based). Use -1 to append at end and execute. cell_source: Code source Returns: list[Union[str, ImageContent]]: List of outputs from the executed cell """ return await __safe_notebook_operation( lambda: insert_execute_code_cell_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, cell_index=cell_index, cell_source=cell_source, ensure_kernel_alive=__ensure_kernel_alive, ) ) @mcp.tool() async def overwrite_cell_source(cell_index: int, cell_source: str) -> str: """Overwrite the source of an existing cell. Note this does not execute the modified cell by itself. Args: cell_index: Index of the cell to overwrite (0-based) cell_source: New cell source - must match existing cell type Returns: str: Success message with diff showing changes made """ return await __safe_notebook_operation( lambda: overwrite_cell_source_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, cell_index=cell_index, cell_source=cell_source, ) ) @mcp.tool() async def execute_cell(cell_index: int, timeout_seconds: int = 300, stream: bool = False, progress_interval: int = 5) -> list[Union[str, ImageContent]]: """Execute a cell with configurable timeout and optional streaming progress updates. Args: cell_index: Index of the cell to execute (0-based) timeout_seconds: Maximum time to wait for execution (default: 300s) stream: Enable streaming progress updates for long-running cells (default: False) progress_interval: Seconds between progress updates when stream=True (default: 5s) Returns: list[Union[str, ImageContent]]: List of outputs from the executed cell """ return await __safe_notebook_operation( lambda: execute_cell_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, cell_index=cell_index, timeout_seconds=timeout_seconds, stream=stream, progress_interval=progress_interval, ensure_kernel_alive_fn=__ensure_kernel_alive, wait_for_kernel_idle_fn=__wait_for_kernel_idle, safe_extract_outputs_fn=safe_extract_outputs, execute_cell_with_forced_sync_fn=__execute_cell_with_forced_sync, extract_output_fn=extract_output, ), max_retries=1 ) @mcp.tool() async def read_cells() -> list[dict[str, Union[str, int, list[Union[str, ImageContent]]]]]: """Read all cells from the Jupyter notebook. Returns: list[dict]: List of cell information including index, type, source, and outputs (for code cells) """ return await __safe_notebook_operation( lambda: read_cells_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, notebook_manager=notebook_manager, ) ) @mcp.tool() async def list_cells() -> str: """List the basic information of all cells in the notebook. Returns a formatted table showing the index, type, execution count (for code cells), and first line of each cell. This provides a quick overview of the notebook structure and is useful for locating specific cells for operations like delete or insert. Returns: str: Formatted table with cell information (Index, Type, Count, First Line) """ return await __safe_notebook_operation( lambda: list_cells_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, notebook_manager=notebook_manager, ) ) @mcp.tool() async def read_cell(cell_index: int) -> dict[str, Union[str, int, list[Union[str, ImageContent]]]]: """Read a specific cell from the Jupyter notebook. Args: cell_index: Index of the cell to read (0-based) Returns: dict: Cell information including index, type, source, and outputs (for code cells) """ return await __safe_notebook_operation( lambda: read_cell_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, notebook_manager=notebook_manager, cell_index=cell_index, ) ) @mcp.tool() async def delete_cell(cell_index: int) -> str: """Delete a specific cell from the Jupyter notebook. Args: cell_index: Index of the cell to delete (0-based) Returns: str: Success message """ return await __safe_notebook_operation( lambda: delete_cell_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, cell_index=cell_index, ) ) @mcp.tool() async def execute_ipython(code: str, timeout: int = 60) -> list[Union[str, ImageContent]]: """Execute IPython code directly in the kernel on the current active notebook. This powerful tool supports: 1. Magic commands (e.g., %timeit, %who, %load, %run, %matplotlib) 2. Shell commands (e.g., !pip install, !ls, !cat) 3. Python code (e.g., print(df.head()), df.info()) Use cases: - Performance profiling and debugging - Environment exploration and package management - Variable inspection and data analysis - File system operations on Jupyter server - Temporary calculations and quick tests Args: code: IPython code to execute (supports magic commands, shell commands with !, and Python code) timeout: Execution timeout in seconds (default: 60s) Returns: List of outputs from the executed code """ # Get kernel_id for JUPYTER_SERVER mode # Let the tool handle getting kernel_id via get_current_notebook_context() kernel_id = None if server_context.mode == ServerMode.JUPYTER_SERVER: current_notebook = notebook_manager.get_current_notebook() or "default" kernel_id = notebook_manager.get_kernel_id(current_notebook) # Note: kernel_id might be None here if notebook not in manager, # but the tool will fall back to config values via get_current_notebook_context() return await __safe_notebook_operation( lambda: execute_ipython_tool.execute( mode=server_context.mode, server_client=server_context.server_client, kernel_manager=server_context.kernel_manager, notebook_manager=notebook_manager, code=code, timeout=timeout, kernel_id=kernel_id, ensure_kernel_alive_fn=__ensure_kernel_alive, wait_for_kernel_idle_fn=__wait_for_kernel_idle, safe_extract_outputs_fn=safe_extract_outputs, ), max_retries=1 ) @mcp.tool() async def list_files(path: str = "", max_depth: int = 3) -> str: """List all files and directories in the Jupyter server's file system. This tool recursively lists files and directories from the Jupyter server's content API, showing the complete file structure including notebooks, data files, scripts, and directories. Args: path: The starting path to list from (empty string means root directory) max_depth: Maximum depth to recurse into subdirectories (default: 3) Returns: str: Tab-separated table with columns: Path, Type, Size, Last_Modified """ return await __safe_notebook_operation( lambda: list_files_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, path=path, max_depth=max_depth, list_files_recursively_fn=_list_files_recursively, ) ) @mcp.tool() async def list_kernels() -> str: """List all available kernels in the Jupyter server. This tool shows all running and available kernel sessions on the Jupyter server, including their IDs, names, states, connection information, and kernel specifications. Useful for monitoring kernel resources and identifying specific kernels for connection. Returns: str: Tab-separated table with columns: ID, Name, Display_Name, Language, State, Connections, Last_Activity, Environment """ return await __safe_notebook_operation( lambda: list_kernel_tool.execute( mode=server_context.mode, server_client=server_context.server_client, kernel_manager=server_context.kernel_manager, kernel_spec_manager=server_context.kernel_spec_manager, ) ) @mcp.tool() async def assign_kernel_to_notebook( notebook_path: str, kernel_id: str, session_name: str = None ) -> str: """Assign a kernel to a notebook by creating a Jupyter session. This creates a Jupyter server session that connects a notebook file to a kernel, enabling code execution in the notebook. Sessions are the mechanism Jupyter uses to maintain the relationship between notebooks and their kernels. Args: notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb") kernel_id: ID of the kernel to assign to the notebook session_name: Optional name for the session (defaults to notebook path) Returns: str: Success message with session information including session ID """ return await __safe_notebook_operation( lambda: assign_kernel_to_notebook_tool.execute( mode=server_context.mode, server_client=server_context.server_client, contents_manager=server_context.contents_manager, session_manager=server_context.session_manager, kernel_manager=server_context.kernel_manager, notebook_path=notebook_path, kernel_id=kernel_id, session_name=session_name, ) ) ############################################################################### # Helper Functions for Extension. async def get_registered_tools(): """ Get list of all registered MCP tools with their metadata. This function is used by the Jupyter extension to dynamically expose the tool registry without hardcoding tool names and parameters. Returns: list: List of tool dictionaries with name, description, and inputSchema """ # Use FastMCP's list_tools method which returns Tool objects tools_list = await mcp.list_tools() tools = [] for tool in tools_list: tool_dict = { "name": tool.name, "description": tool.description, } # Extract parameter names from inputSchema if hasattr(tool, 'inputSchema') and tool.inputSchema: input_schema = tool.inputSchema if 'properties' in input_schema: tool_dict["parameters"] = list(input_schema['properties'].keys()) else: tool_dict["parameters"] = [] # Include full inputSchema for MCP protocol compatibility tool_dict["inputSchema"] = input_schema else: tool_dict["parameters"] = [] tools.append(tool_dict) return tools ############################################################################### # Commands. # Shared options decorator to reduce code duplication def _common_options(f): """Decorator that adds common start options to a command.""" options = [ click.option( "--provider", envvar="PROVIDER", type=click.Choice(["jupyter", "datalayer"]), default="jupyter", help="The provider to use for the document and runtime. Defaults to 'jupyter'.", ), click.option( "--runtime-url", envvar="RUNTIME_URL", type=click.STRING, default="http://localhost:8888", help="The runtime URL to use. For the jupyter provider, this is the Jupyter server URL. For the datalayer provider, this is the Datalayer runtime URL.", ), click.option( "--runtime-id", envvar="RUNTIME_ID", type=click.STRING, default=None, help="The kernel ID to use. If not provided, a new kernel should be started.", ), click.option( "--runtime-token", envvar="RUNTIME_TOKEN", type=click.STRING, default=None, help="The runtime token to use for authentication with the provider. If not provided, the provider should accept anonymous requests.", ), click.option( "--document-url", envvar="DOCUMENT_URL", type=click.STRING, default="http://localhost:8888", help="The document URL to use. For the jupyter provider, this is the Jupyter server URL. For the datalayer provider, this is the Datalayer document URL.", ), click.option( "--document-id", envvar="DOCUMENT_ID", type=click.STRING, default=None, help="The document id to use. For the jupyter provider, this is the notebook path. For the datalayer provider, this is the notebook path. Optional - if omitted, you can list and select notebooks interactively.", ), click.option( "--document-token", envvar="DOCUMENT_TOKEN", type=click.STRING, default=None, help="The document token to use for authentication with the provider. If not provided, the provider should accept anonymous requests.", ) ] # Apply decorators in reverse order for option in reversed(options): f = option(f) return f def _do_start( transport: str, start_new_runtime: bool, runtime_url: str, runtime_id: str, runtime_token: str, document_url: str, document_id: str, document_token: str, port: int, provider: str, ): """Internal function to execute the start logic.""" # Log the received configuration for diagnostics # Note: set_config() will automatically normalize string "None" values logger.info( f"Start command received - runtime_url: {repr(runtime_url)}, " f"document_url: {repr(document_url)}, provider: {provider}, " f"transport: {transport}" ) # Set configuration using the singleton # String "None" values will be automatically normalized by set_config() config = set_config( transport=transport, provider=provider, runtime_url=runtime_url, start_new_runtime=start_new_runtime, runtime_id=runtime_id, runtime_token=runtime_token, document_url=document_url, document_id=document_id, document_token=document_token, port=port ) # Reset ServerContext to pick up new configuration ServerContext.reset() # Determine startup behavior based on configuration if config.document_id: # If document_id is provided, auto-enroll the notebook # Kernel creation depends on start_new_runtime and runtime_id flags try: import asyncio # Run the async enrollment in the event loop asyncio.run(__auto_enroll_document()) except Exception as e: logger.error(f"Failed to auto-enroll document '{config.document_id}': {e}") # Fallback to legacy kernel-only mode if enrollment fails if config.start_new_runtime or config.runtime_id: try: __start_kernel() except Exception as e2: logger.error(f"Failed to start kernel on startup: {e2}") elif config.start_new_runtime or config.runtime_id: # If no document_id but start_new_runtime/runtime_id is set, just create kernel # This is for backward compatibility - kernel without managed notebook try: __start_kernel() except Exception as e: logger.error(f"Failed to start kernel on startup: {e}") # else: No startup action - user must manually enroll notebooks or create kernels logger.info(f"Starting Jupyter MCP Server with transport: {transport}") if transport == "stdio": mcp.run(transport="stdio") elif transport == "streamable-http": uvicorn.run(mcp.streamable_http_app, host="0.0.0.0", port=port) # noqa: S104 else: raise Exception("Transport should be `stdio` or `streamable-http`.") @click.group(invoke_without_command=True) @_common_options @click.option( "--transport", envvar="TRANSPORT", type=click.Choice(["stdio", "streamable-http"]), default="stdio", help="The transport to use for the MCP server. Defaults to 'stdio'.", ) @click.option( "--start-new-runtime", envvar="START_NEW_RUNTIME", type=click.BOOL, default=True, help="Start a new runtime or use an existing one.", ) @click.option( "--port", envvar="PORT", type=click.INT, default=4040, help="The port to use for the Streamable HTTP transport. Ignored for stdio transport.", ) @click.pass_context def server( ctx, transport: str, start_new_runtime: bool, runtime_url: str, runtime_id: str, runtime_token: str, document_url: str, document_id: str, document_token: str, port: int, provider: str, ): """Manages Jupyter MCP Server. When invoked without subcommands, starts the MCP server directly. This allows for quick startup with: uvx jupyter-mcp-server Subcommands (start, connect, stop) are still available for advanced use cases. """ # If a subcommand is invoked, let it handle the execution if ctx.invoked_subcommand is not None: return # No subcommand provided - execute the default start behavior _do_start( transport=transport, start_new_runtime=start_new_runtime, runtime_url=runtime_url, runtime_id=runtime_id, runtime_token=runtime_token, document_url=document_url, document_id=document_id, document_token=document_token, port=port, provider=provider, ) @server.command("connect") @_common_options @click.option( "--jupyter-mcp-server-url", envvar="JUPYTER_MCP_SERVER_URL", type=click.STRING, default="http://localhost:4040", help="The URL of the Jupyter MCP Server to connect to. Defaults to 'http://localhost:4040'.", ) def connect_command( jupyter_mcp_server_url: str, runtime_url: str, runtime_id: str, runtime_token: str, document_url: str, document_id: str, document_token: str, provider: str, ): """Command to connect a Jupyter MCP Server to a document and a runtime.""" # Set configuration using the singleton set_config( provider=provider, runtime_url=runtime_url, runtime_id=runtime_id, runtime_token=runtime_token, document_url=document_url, document_id=document_id, document_token=document_token ) config = get_config() document_runtime = DocumentRuntime( provider=config.provider, runtime_url=config.runtime_url, runtime_id=config.runtime_id, runtime_token=config.runtime_token, document_url=config.document_url, document_id=config.document_id, document_token=config.document_token, ) r = httpx.put( f"{jupyter_mcp_server_url}/api/connect", headers={ "Content-Type": "application/json", "Accept": "application/json", }, content=document_runtime.model_dump_json(), ) r.raise_for_status() @server.command("stop") @click.option( "--jupyter-mcp-server-url", envvar="JUPYTER_MCP_SERVER_URL", type=click.STRING, default="http://localhost:4040", help="The URL of the Jupyter MCP Server to stop. Defaults to 'http://localhost:4040'.", ) def stop_command(jupyter_mcp_server_url: str): r = httpx.delete( f"{jupyter_mcp_server_url}/api/stop", ) r.raise_for_status() @server.command("start") @_common_options @click.option( "--transport", envvar="TRANSPORT", type=click.Choice(["stdio", "streamable-http"]), default="stdio", help="The transport to use for the MCP server. Defaults to 'stdio'.", ) @click.option( "--start-new-runtime", envvar="START_NEW_RUNTIME", type=click.BOOL, default=True, help="Start a new runtime or use an existing one.", ) @click.option( "--port", envvar="PORT", type=click.INT, default=4040, help="The port to use for the Streamable HTTP transport. Ignored for stdio transport.", ) def start_command( transport: str, start_new_runtime: bool, runtime_url: str, runtime_id: str, runtime_token: str, document_url: str, document_id: str, document_token: str, port: int, provider: str, ): """Start the Jupyter MCP server with a transport.""" _do_start( transport=transport, start_new_runtime=start_new_runtime, runtime_url=runtime_url, runtime_id=runtime_id, runtime_token=runtime_token, document_url=document_url, document_id=document_id, document_token=document_token, port=port, provider=provider, ) ############################################################################### # Main. if __name__ == "__main__": start_command() ```