#
tokens: 37155/50000 2/134 files (page 5/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 6. Use http://codebase.md/datalayer/jupyter-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .github
│   ├── copilot-instructions.md
│   ├── dependabot.yml
│   └── workflows
│       ├── build.yml
│       ├── fix-license-header.yml
│       ├── lint.sh
│       ├── prep-release.yml
│       ├── publish-release.yml
│       └── test.yml
├── .gitignore
├── .licenserc.yaml
├── .pre-commit-config.yaml
├── .vscode
│   ├── mcp.json
│   └── settings.json
├── ARCHITECTURE.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── dev
│   ├── content
│   │   ├── new.ipynb
│   │   ├── notebook.ipynb
│   │   └── README.md
│   └── README.md
├── Dockerfile
├── docs
│   ├── .gitignore
│   ├── .yarnrc.yml
│   ├── babel.config.js
│   ├── docs
│   │   ├── _category_.yaml
│   │   ├── clients
│   │   │   ├── _category_.yaml
│   │   │   ├── claude_desktop
│   │   │   │   ├── _category_.yaml
│   │   │   │   └── index.mdx
│   │   │   ├── cline
│   │   │   │   ├── _category_.yaml
│   │   │   │   └── index.mdx
│   │   │   ├── cursor
│   │   │   │   ├── _category_.yaml
│   │   │   │   └── index.mdx
│   │   │   ├── index.mdx
│   │   │   ├── vscode
│   │   │   │   ├── _category_.yaml
│   │   │   │   └── index.mdx
│   │   │   └── windsurf
│   │   │       ├── _category_.yaml
│   │   │       └── index.mdx
│   │   ├── configure
│   │   │   ├── _category_.yaml
│   │   │   └── index.mdx
│   │   ├── contribute
│   │   │   ├── _category_.yaml
│   │   │   └── index.mdx
│   │   ├── deployment
│   │   │   ├── _category_.yaml
│   │   │   ├── datalayer
│   │   │   │   ├── _category_.yaml
│   │   │   │   └── streamable-http
│   │   │   │       └── index.mdx
│   │   │   ├── index.mdx
│   │   │   └── jupyter
│   │   │       ├── _category_.yaml
│   │   │       ├── index.mdx
│   │   │       ├── stdio
│   │   │       │   ├── _category_.yaml
│   │   │       │   └── index.mdx
│   │   │       └── streamable-http
│   │   │           ├── _category_.yaml
│   │   │           ├── jupyter-extension
│   │   │           │   └── index.mdx
│   │   │           └── standalone
│   │   │               └── index.mdx
│   │   ├── index.mdx
│   │   ├── releases
│   │   │   ├── _category_.yaml
│   │   │   └── index.mdx
│   │   ├── resources
│   │   │   ├── _category_.yaml
│   │   │   └── index.mdx
│   │   └── tools
│   │       ├── _category_.yaml
│   │       └── index.mdx
│   ├── docusaurus.config.js
│   ├── LICENSE
│   ├── Makefile
│   ├── package.json
│   ├── README.md
│   ├── sidebars.js
│   ├── src
│   │   ├── components
│   │   │   ├── HomepageFeatures.js
│   │   │   ├── HomepageFeatures.module.css
│   │   │   ├── HomepageProducts.js
│   │   │   └── HomepageProducts.module.css
│   │   ├── css
│   │   │   └── custom.css
│   │   ├── pages
│   │   │   ├── index.module.css
│   │   │   ├── markdown-page.md
│   │   │   └── testimonials.tsx
│   │   └── theme
│   │       └── CustomDocItem.tsx
│   └── static
│       └── img
│           ├── datalayer
│           │   ├── logo.png
│           │   └── logo.svg
│           ├── favicon.ico
│           ├── feature_1.svg
│           ├── feature_2.svg
│           ├── feature_3.svg
│           ├── product_1.svg
│           ├── product_2.svg
│           └── product_3.svg
├── examples
│   └── integration_example.py
├── jupyter_mcp_server
│   ├── __init__.py
│   ├── __main__.py
│   ├── __version__.py
│   ├── config.py
│   ├── enroll.py
│   ├── env.py
│   ├── jupyter_extension
│   │   ├── __init__.py
│   │   ├── backends
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── local_backend.py
│   │   │   └── remote_backend.py
│   │   ├── context.py
│   │   ├── extension.py
│   │   ├── handlers.py
│   │   └── protocol
│   │       ├── __init__.py
│   │       └── messages.py
│   ├── models.py
│   ├── notebook_manager.py
│   ├── server_modes.py
│   ├── server.py
│   ├── tools
│   │   ├── __init__.py
│   │   ├── _base.py
│   │   ├── _registry.py
│   │   ├── assign_kernel_to_notebook_tool.py
│   │   ├── delete_cell_tool.py
│   │   ├── execute_cell_tool.py
│   │   ├── execute_ipython_tool.py
│   │   ├── insert_cell_tool.py
│   │   ├── insert_execute_code_cell_tool.py
│   │   ├── list_cells_tool.py
│   │   ├── list_files_tool.py
│   │   ├── list_kernels_tool.py
│   │   ├── list_notebooks_tool.py
│   │   ├── overwrite_cell_source_tool.py
│   │   ├── read_cell_tool.py
│   │   ├── read_cells_tool.py
│   │   ├── restart_notebook_tool.py
│   │   ├── unuse_notebook_tool.py
│   │   └── use_notebook_tool.py
│   └── utils.py
├── jupyter-config
│   ├── jupyter_notebook_config
│   │   └── jupyter_mcp_server.json
│   └── jupyter_server_config.d
│       └── jupyter_mcp_server.json
├── LICENSE
├── Makefile
├── pyproject.toml
├── pytest.ini
├── README.md
├── RELEASE.md
├── smithery.yaml
└── tests
    ├── __init__.py
    ├── conftest.py
    ├── test_common.py
    ├── test_config.py
    ├── test_jupyter_extension.py
    ├── test_list_kernels.py
    ├── test_tools.py
    └── test_use_notebook.py
```

# Files

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

```
  1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2 | <!--
  3 |   ~ Copyright (c) 2023-2024 Datalayer, Inc.
  4 |   ~
  5 |   ~ BSD 3-Clause License
  6 | -->
  7 | 
  8 | <svg
  9 |    xmlns:figma="http://www.figma.com/figma/ns"
 10 |    xmlns:dc="http://purl.org/dc/elements/1.1/"
 11 |    xmlns:cc="http://creativecommons.org/ns#"
 12 |    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 13 |    xmlns:svg="http://www.w3.org/2000/svg"
 14 |    xmlns="http://www.w3.org/2000/svg"
 15 |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 16 |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 17 |    viewBox="0 0 335.67566 308.79167"
 18 |    version="1.1"
 19 |    id="svg1327"
 20 |    sodipodi:docname="2.svg"
 21 |    inkscape:version="1.0.1 (c497b03c, 2020-09-10)"
 22 |    width="335.67566"
 23 |    height="308.79166">
 24 |   <metadata
 25 |      id="metadata1331">
 26 |     <rdf:RDF>
 27 |       <cc:Work
 28 |          rdf:about="">
 29 |         <dc:format>image/svg+xml</dc:format>
 30 |         <dc:type
 31 |            rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
 32 |         <dc:title>Web_hosting_SVG</dc:title>
 33 |       </cc:Work>
 34 |     </rdf:RDF>
 35 |   </metadata>
 36 |   <sodipodi:namedview
 37 |      pagecolor="#ffffff"
 38 |      bordercolor="#666666"
 39 |      borderopacity="1"
 40 |      objecttolerance="10"
 41 |      gridtolerance="10"
 42 |      guidetolerance="10"
 43 |      inkscape:pageopacity="0"
 44 |      inkscape:pageshadow="2"
 45 |      inkscape:window-width="1357"
 46 |      inkscape:window-height="701"
 47 |      id="namedview1329"
 48 |      showgrid="false"
 49 |      inkscape:zoom="0.98691435"
 50 |      inkscape:cx="142.90769"
 51 |      inkscape:cy="115.1505"
 52 |      inkscape:window-x="0"
 53 |      inkscape:window-y="25"
 54 |      inkscape:window-maximized="0"
 55 |      inkscape:current-layer="svg1327"
 56 |      inkscape:document-rotation="0"
 57 |      fit-margin-top="0"
 58 |      fit-margin-left="0"
 59 |      fit-margin-right="0"
 60 |      fit-margin-bottom="0" />
 61 |   <defs
 62 |      id="defs835">
 63 |     <style
 64 |        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>
 65 |   </defs>
 66 |   <title
 67 |      id="title837">Web_hosting_SVG</title>
 68 |   <polygon
 69 |      class="cls-1"
 70 |      points="249.36,272.19 251.14,273.21 107.37,356.22 106.48,354.68 "
 71 |      id="polygon839"
 72 |      transform="translate(0.00186273,-119.51243)" />
 73 |   <path
 74 |      class="cls-1"
 75 |      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"
 76 |      id="path1155" />
 77 |   <path
 78 |      class="cls-7"
 79 |      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"
 80 |      id="path1157" />
 81 |   <path
 82 |      class="cls-8"
 83 |      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"
 84 |      id="path1159" />
 85 |   <path
 86 |      class="cls-9"
 87 |      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"
 88 |      id="path1161" />
 89 |   <g
 90 |      id="_Группа_"
 91 |      data-name="&lt;Группа&gt;"
 92 |      transform="translate(0.00186273,-119.51243)">
 93 |     <path
 94 |        id="_Контур_"
 95 |        data-name="&lt;Контур&gt;"
 96 |        class="cls-10"
 97 |        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" />
 98 |     <path
 99 |        id="_Контур_2"
100 |        data-name="&lt;Контур&gt;"
101 |        class="cls-10"
102 |        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" />
103 |     <g
104 |        id="_Группа_2"
105 |        data-name="&lt;Группа&gt;">
106 |       <path
107 |          id="_Контур_3"
108 |          data-name="&lt;Контур&gt;"
109 |          class="cls-11"
110 |          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" />
111 |     </g>
112 |     <g
113 |        id="_Группа_3"
114 |        data-name="&lt;Группа&gt;">
115 |       <polygon
116 |          id="_Контур_4"
117 |          data-name="&lt;Контур&gt;"
118 |          class="cls-12"
119 |          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 " />
120 |       <polygon
121 |          id="_Контур_5"
122 |          data-name="&lt;Контур&gt;"
123 |          class="cls-10"
124 |          points="66.33,328.67 56.48,322.98 51.08,326.1 60.93,331.79 " />
125 |     </g>
126 |     <path
127 |        class="cls-5"
128 |        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"
129 |        id="path1170" />
130 |     <path
131 |        class="cls-5"
132 |        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"
133 |        id="path1172" />
134 |     <path
135 |        class="cls-5"
136 |        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"
137 |        id="path1174" />
138 |     <path
139 |        class="cls-5"
140 |        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"
141 |        id="path1176" />
142 |     <path
143 |        class="cls-5"
144 |        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"
145 |        id="path1178" />
146 |     <path
147 |        class="cls-5"
148 |        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"
149 |        id="path1180" />
150 |     <path
151 |        class="cls-5"
152 |        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"
153 |        id="path1182" />
154 |     <path
155 |        class="cls-5"
156 |        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"
157 |        id="path1184" />
158 |     <path
159 |        class="cls-5"
160 |        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"
161 |        id="path1186" />
162 |     <path
163 |        class="cls-5"
164 |        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"
165 |        id="path1188" />
166 |     <path
167 |        class="cls-5"
168 |        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"
169 |        id="path1190" />
170 |     <path
171 |        class="cls-5"
172 |        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"
173 |        id="path1192" />
174 |     <path
175 |        class="cls-5"
176 |        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"
177 |        id="path1194" />
178 |     <path
179 |        class="cls-5"
180 |        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"
181 |        id="path1196" />
182 |     <path
183 |        class="cls-5"
184 |        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"
185 |        id="path1198" />
186 |     <path
187 |        class="cls-5"
188 |        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"
189 |        id="path1200" />
190 |     <path
191 |        class="cls-5"
192 |        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"
193 |        id="path1202" />
194 |     <path
195 |        class="cls-5"
196 |        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"
197 |        id="path1204" />
198 |     <path
199 |        class="cls-5"
200 |        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"
201 |        id="path1206" />
202 |     <path
203 |        class="cls-5"
204 |        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"
205 |        id="path1208" />
206 |     <path
207 |        class="cls-5"
208 |        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"
209 |        id="path1210" />
210 |     <path
211 |        class="cls-5"
212 |        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"
213 |        id="path1212" />
214 |     <path
215 |        class="cls-5"
216 |        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"
217 |        id="path1214" />
218 |     <path
219 |        class="cls-5"
220 |        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"
221 |        id="path1216" />
222 |     <path
223 |        class="cls-5"
224 |        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"
225 |        id="path1218" />
226 |     <path
227 |        class="cls-5"
228 |        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"
229 |        id="path1220" />
230 |     <path
231 |        class="cls-5"
232 |        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"
233 |        id="path1222" />
234 |     <path
235 |        class="cls-5"
236 |        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"
237 |        id="path1224" />
238 |     <path
239 |        class="cls-5"
240 |        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"
241 |        id="path1226" />
242 |     <path
243 |        class="cls-5"
244 |        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"
245 |        id="path1228" />
246 |     <path
247 |        class="cls-5"
248 |        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"
249 |        id="path1230" />
250 |     <path
251 |        class="cls-5"
252 |        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"
253 |        id="path1232" />
254 |     <path
255 |        class="cls-5"
256 |        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"
257 |        id="path1234" />
258 |     <path
259 |        class="cls-5"
260 |        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"
261 |        id="path1236" />
262 |     <path
263 |        class="cls-5"
264 |        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"
265 |        id="path1238" />
266 |     <path
267 |        class="cls-5"
268 |        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"
269 |        id="path1240" />
270 |     <path
271 |        class="cls-5"
272 |        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"
273 |        id="path1242" />
274 |     <path
275 |        class="cls-5"
276 |        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"
277 |        id="path1244" />
278 |     <path
279 |        class="cls-5"
280 |        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"
281 |        id="path1246" />
282 |     <path
283 |        class="cls-5"
284 |        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"
285 |        id="path1248" />
286 |     <path
287 |        class="cls-5"
288 |        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"
289 |        id="path1250" />
290 |     <path
291 |        class="cls-5"
292 |        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"
293 |        id="path1252" />
294 |     <path
295 |        class="cls-5"
296 |        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"
297 |        id="path1254" />
298 |     <path
299 |        class="cls-5"
300 |        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"
301 |        id="path1256" />
302 |     <path
303 |        class="cls-5"
304 |        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"
305 |        id="path1258" />
306 |     <path
307 |        class="cls-5"
308 |        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"
309 |        id="path1260" />
310 |     <path
311 |        class="cls-5"
312 |        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"
313 |        id="path1262" />
314 |     <path
315 |        class="cls-5"
316 |        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"
317 |        id="path1264" />
318 |     <path
319 |        class="cls-5"
320 |        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"
321 |        id="path1266" />
322 |     <path
323 |        class="cls-5"
324 |        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"
325 |        id="path1268" />
326 |     <path
327 |        class="cls-5"
328 |        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"
329 |        id="path1270" />
330 |     <path
331 |        class="cls-5"
332 |        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"
333 |        id="path1272" />
334 |     <path
335 |        class="cls-5"
336 |        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"
337 |        id="path1274" />
338 |     <path
339 |        class="cls-5"
340 |        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"
341 |        id="path1276" />
342 |     <path
343 |        class="cls-5"
344 |        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"
345 |        id="path1278" />
346 |     <path
347 |        class="cls-5"
348 |        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"
349 |        id="path1280" />
350 |     <path
351 |        class="cls-5"
352 |        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"
353 |        id="path1282" />
354 |     <path
355 |        class="cls-5"
356 |        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"
357 |        id="path1284" />
358 |     <path
359 |        class="cls-5"
360 |        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"
361 |        id="path1286" />
362 |     <path
363 |        class="cls-5"
364 |        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"
365 |        id="path1288" />
366 |     <path
367 |        class="cls-5"
368 |        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"
369 |        id="path1290" />
370 |     <path
371 |        class="cls-5"
372 |        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"
373 |        id="path1292" />
374 |     <path
375 |        class="cls-5"
376 |        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"
377 |        id="path1294" />
378 |     <path
379 |        class="cls-5"
380 |        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"
381 |        id="path1296" />
382 |   </g>
383 |   <g
384 |      id="_Группа_4"
385 |      data-name="&lt;Группа&gt;"
386 |      transform="translate(0.00186273,-119.51243)">
387 |     <g
388 |        id="_Группа_5"
389 |        data-name="&lt;Группа&gt;">
390 |       <path
391 |          id="_Контур_6"
392 |          data-name="&lt;Контур&gt;"
393 |          class="cls-10"
394 |          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" />
395 |       <path
396 |          id="_Контур_7"
397 |          data-name="&lt;Контур&gt;"
398 |          class="cls-10"
399 |          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" />
400 |       <path
401 |          id="_Контур_8"
402 |          data-name="&lt;Контур&gt;"
403 |          class="cls-13"
404 |          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" />
405 |       <path
406 |          id="_Контур_9"
407 |          data-name="&lt;Контур&gt;"
408 |          class="cls-11"
409 |          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" />
410 |       <polygon
411 |          id="_Контур_10"
412 |          data-name="&lt;Контур&gt;"
413 |          class="cls-14"
414 |          points="60.93,306.48 60.93,307.4 93.16,326.01 93.16,325.09 " />
415 |     </g>
416 |     <polygon
417 |        id="_Контур_11"
418 |        data-name="&lt;Контур&gt;"
419 |        class="cls-15"
420 |        points="92.12,323.11 92.12,300.34 62.16,283.04 62.16,305.86 " />
421 |   </g>
422 |   <g
423 |      id="Men_2"
424 |      transform="translate(0.00186273,-119.51243)">
425 |     <path
426 |        id="_Контур_12"
427 |        data-name="&lt;Контур&gt;"
428 |        class="cls-16"
429 |        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" />
430 |     <path
431 |        id="_Контур_13"
432 |        data-name="&lt;Контур&gt;"
433 |        class="cls-17"
434 |        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" />
435 |     <path
436 |        class="cls-18"
437 |        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"
438 |        id="path1309" />
439 |     <path
440 |        class="cls-19"
441 |        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"
442 |        id="path1311" />
443 |     <path
444 |        class="cls-18"
445 |        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"
446 |        id="path1313" />
447 |     <path
448 |        class="cls-19"
449 |        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"
450 |        id="path1315" />
451 |     <path
452 |        id="_Контур_14"
453 |        data-name="&lt;Контур&gt;"
454 |        class="cls-6"
455 |        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" />
456 |     <path
457 |        id="_Контур_15"
458 |        data-name="&lt;Контур&gt;"
459 |        class="cls-16"
460 |        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" />
461 |     <path
462 |        id="_Контур_16"
463 |        data-name="&lt;Контур&gt;"
464 |        class="cls-11"
465 |        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" />
466 |     <path
467 |        id="_Контур_17"
468 |        data-name="&lt;Контур&gt;"
469 |        class="cls-16"
470 |        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" />
471 |     <path
472 |        id="_Контур_18"
473 |        data-name="&lt;Контур&gt;"
474 |        class="cls-17"
475 |        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" />
476 |     <path
477 |        class="cls-20"
478 |        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"
479 |        id="path1322" />
480 |   </g>
481 |   <g
482 |      id="Canvas"
483 |      transform="matrix(4.0495342,0,0,4.0495342,-6480.5841,-9936.3011)"
484 |      figma:type="canvas">
485 |     <g
486 |        id="g2566"
487 |        style="mix-blend-mode:normal"
488 |        figma:type="group">
489 |       <g
490 |          id="g2564"
491 |          style="mix-blend-mode:normal"
492 |          figma:type="group">
493 |         <g
494 |            id="Group"
495 |            style="mix-blend-mode:normal"
496 |            figma:type="group">
497 |           <g
498 |              id="g"
499 |              style="mix-blend-mode:normal"
500 |              figma:type="group">
501 |             <g
502 |                id="path"
503 |                style="mix-blend-mode:normal"
504 |                figma:type="group">
505 |               <g
506 |                  id="path9 fill"
507 |                  style="mix-blend-mode:normal"
508 |                  figma:type="vector">
509 |                 <path
510 |                    id="use2501"
511 |                    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"
512 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
513 |               </g>
514 |             </g>
515 |             <g
516 |                id="g2508"
517 |                style="mix-blend-mode:normal"
518 |                figma:type="group">
519 |               <g
520 |                  id="path10 fill"
521 |                  style="mix-blend-mode:normal"
522 |                  figma:type="vector">
523 |                 <path
524 |                    id="use2505"
525 |                    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"
526 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
527 |               </g>
528 |             </g>
529 |             <g
530 |                id="g2513"
531 |                style="mix-blend-mode:normal"
532 |                figma:type="group">
533 |               <g
534 |                  id="path11 fill"
535 |                  style="mix-blend-mode:normal"
536 |                  figma:type="vector">
537 |                 <path
538 |                    id="use2510"
539 |                    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"
540 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
541 |               </g>
542 |             </g>
543 |             <g
544 |                id="g2518"
545 |                style="mix-blend-mode:normal"
546 |                figma:type="group">
547 |               <g
548 |                  id="path12 fill"
549 |                  style="mix-blend-mode:normal"
550 |                  figma:type="vector">
551 |                 <path
552 |                    id="use2515"
553 |                    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"
554 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
555 |               </g>
556 |             </g>
557 |             <g
558 |                id="g2523"
559 |                style="mix-blend-mode:normal"
560 |                figma:type="group">
561 |               <g
562 |                  id="path13 fill"
563 |                  style="mix-blend-mode:normal"
564 |                  figma:type="vector">
565 |                 <path
566 |                    id="use2520"
567 |                    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"
568 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
569 |               </g>
570 |             </g>
571 |             <g
572 |                id="g2528"
573 |                style="mix-blend-mode:normal"
574 |                figma:type="group">
575 |               <g
576 |                  id="path14 fill"
577 |                  style="mix-blend-mode:normal"
578 |                  figma:type="vector">
579 |                 <path
580 |                    id="use2525"
581 |                    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"
582 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
583 |               </g>
584 |             </g>
585 |             <g
586 |                id="g2533"
587 |                style="mix-blend-mode:normal"
588 |                figma:type="group">
589 |               <g
590 |                  id="path15 fill"
591 |                  style="mix-blend-mode:normal"
592 |                  figma:type="vector">
593 |                 <path
594 |                    id="use2530"
595 |                    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"
596 |                    style="mix-blend-mode:normal;fill:#4e4e4e" />
597 |               </g>
598 |             </g>
599 |           </g>
600 |         </g>
601 |         <g
602 |            id="g2562"
603 |            style="mix-blend-mode:normal"
604 |            figma:type="group">
605 |           <g
606 |              id="g2540"
607 |              style="mix-blend-mode:normal"
608 |              figma:type="group">
609 |             <g
610 |                id="path16 fill"
611 |                style="mix-blend-mode:normal"
612 |                figma:type="vector">
613 |               <path
614 |                  id="use2537"
615 |                  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"
616 |                  style="mix-blend-mode:normal;fill:#767677" />
617 |             </g>
618 |           </g>
619 |           <g
620 |              id="g2545"
621 |              style="mix-blend-mode:normal"
622 |              figma:type="group">
623 |             <g
624 |                id="path17 fill"
625 |                style="mix-blend-mode:normal"
626 |                figma:type="vector">
627 |               <path
628 |                  id="use2542"
629 |                  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"
630 |                  style="mix-blend-mode:normal;fill:#f37726" />
631 |             </g>
632 |           </g>
633 |           <g
634 |              id="g2550"
635 |              style="mix-blend-mode:normal"
636 |              figma:type="group">
637 |             <g
638 |                id="path18 fill"
639 |                style="mix-blend-mode:normal"
640 |                figma:type="vector">
641 |               <path
642 |                  id="use2547"
643 |                  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"
644 |                  style="mix-blend-mode:normal;fill:#f37726" />
645 |             </g>
646 |           </g>
647 |           <g
648 |              id="g2555"
649 |              style="mix-blend-mode:normal"
650 |              figma:type="group">
651 |             <g
652 |                id="path19 fill"
653 |                style="mix-blend-mode:normal"
654 |                figma:type="vector">
655 |               <path
656 |                  id="use2552"
657 |                  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"
658 |                  style="mix-blend-mode:normal;fill:#9e9e9e" />
659 |             </g>
660 |           </g>
661 |           <g
662 |              id="g2560"
663 |              style="mix-blend-mode:normal"
664 |              figma:type="group">
665 |             <g
666 |                id="path20 fill"
667 |                style="mix-blend-mode:normal"
668 |                figma:type="vector">
669 |               <path
670 |                  id="use2557"
671 |                  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"
672 |                  style="mix-blend-mode:normal;fill:#616262" />
673 |             </g>
674 |           </g>
675 |         </g>
676 |       </g>
677 |     </g>
678 |   </g>
679 | </svg>
680 | 
```

--------------------------------------------------------------------------------
/jupyter_mcp_server/server.py:
--------------------------------------------------------------------------------

```python
   1 | # Copyright (c) 2023-2024 Datalayer, Inc.
   2 | #
   3 | # BSD 3-Clause License
   4 | 
   5 | import logging
   6 | import click
   7 | import httpx
   8 | import uvicorn
   9 | from typing import Union, Optional
  10 | from fastapi import Request
  11 | from jupyter_kernel_client import KernelClient
  12 | from jupyter_server_api import JupyterServerClient
  13 | from mcp.server import FastMCP
  14 | from starlette.applications import Starlette
  15 | from starlette.responses import JSONResponse
  16 | from starlette.middleware.cors import CORSMiddleware
  17 | 
  18 | from jupyter_mcp_server.models import DocumentRuntime
  19 | from jupyter_mcp_server.utils import (
  20 |     extract_output, 
  21 |     safe_extract_outputs, 
  22 |     create_kernel,
  23 |     start_kernel,
  24 |     ensure_kernel_alive,
  25 |     execute_cell_with_timeout,
  26 |     execute_cell_with_forced_sync,
  27 |     is_kernel_busy,
  28 |     wait_for_kernel_idle,
  29 |     safe_notebook_operation,
  30 |     list_files_recursively,
  31 | )
  32 | from jupyter_mcp_server.config import get_config, set_config
  33 | from jupyter_mcp_server.notebook_manager import NotebookManager
  34 | from jupyter_mcp_server.enroll import auto_enroll_document
  35 | from jupyter_mcp_server.tools import (
  36 |     # Tool infrastructure
  37 |     ServerMode,
  38 |     # Notebook Management
  39 |     ListNotebooksTool,
  40 |     UseNotebookTool,
  41 |     RestartNotebookTool,
  42 |     UnuseNotebookTool,
  43 |     # Cell Reading
  44 |     ReadCellsTool,
  45 |     ListCellsTool,
  46 |     ReadCellTool,
  47 |     # Cell Writing
  48 |     InsertCellTool,
  49 |     InsertExecuteCodeCellTool,
  50 |     OverwriteCellSourceTool,
  51 |     DeleteCellTool,
  52 |     # Cell Execution
  53 |     ExecuteCellTool,
  54 |     # Other Tools
  55 |     AssignKernelToNotebookTool,
  56 |     ExecuteIpythonTool,
  57 |     ListFilesTool,
  58 |     ListKernelsTool,
  59 | )
  60 | from typing import Literal, Union
  61 | from mcp.types import ImageContent
  62 | 
  63 | 
  64 | ###############################################################################
  65 | 
  66 | 
  67 | logger = logging.getLogger(__name__)
  68 | 
  69 | 
  70 | ###############################################################################
  71 | 
  72 | class FastMCPWithCORS(FastMCP):
  73 |     def streamable_http_app(self) -> Starlette:
  74 |         """Return StreamableHTTP server app with CORS middleware
  75 |         See: https://github.com/modelcontextprotocol/python-sdk/issues/187
  76 |         """
  77 |         # Get the original Starlette app
  78 |         app = super().streamable_http_app()
  79 |         
  80 |         # Add CORS middleware
  81 |         app.add_middleware(
  82 |             CORSMiddleware,
  83 |             allow_origins=["*"],  # In production, should set specific domains
  84 |             allow_credentials=True,
  85 |             allow_methods=["*"],
  86 |             allow_headers=["*"],
  87 |         )
  88 |         return app
  89 |     
  90 |     def sse_app(self, mount_path: str | None = None) -> Starlette:
  91 |         """Return SSE server app with CORS middleware"""
  92 |         # Get the original Starlette app
  93 |         app = super().sse_app(mount_path)
  94 |         # Add CORS middleware
  95 |         app.add_middleware(
  96 |             CORSMiddleware,
  97 |             allow_origins=["*"],  # In production, should set specific domains
  98 |             allow_credentials=True,
  99 |             allow_methods=["*"],
 100 |             allow_headers=["*"],
 101 |         )        
 102 |         return app
 103 | 
 104 | 
 105 | ###############################################################################
 106 | 
 107 | 
 108 | mcp = FastMCPWithCORS(name="Jupyter MCP Server", json_response=False, stateless_http=True)
 109 | 
 110 | # Initialize the unified notebook manager
 111 | notebook_manager = NotebookManager()
 112 | 
 113 | # Initialize all tool instances (no arguments needed - tools receive dependencies via execute())
 114 | # Notebook Management Tools
 115 | list_notebook_tool = ListNotebooksTool()
 116 | use_notebook_tool = UseNotebookTool()
 117 | restart_notebook_tool = RestartNotebookTool()
 118 | unuse_notebook_tool = UnuseNotebookTool()
 119 | 
 120 | # Cell Reading Tools
 121 | read_cells_tool = ReadCellsTool()
 122 | list_cells_tool = ListCellsTool()
 123 | read_cell_tool = ReadCellTool()
 124 | 
 125 | # Cell Writing Tools
 126 | insert_cell_tool = InsertCellTool()
 127 | insert_execute_code_cell_tool = InsertExecuteCodeCellTool()
 128 | overwrite_cell_source_tool = OverwriteCellSourceTool()
 129 | delete_cell_tool = DeleteCellTool()
 130 | 
 131 | # Cell Execution Tools
 132 | execute_cell_tool = ExecuteCellTool()
 133 | 
 134 | # Other Tools
 135 | assign_kernel_to_notebook_tool = AssignKernelToNotebookTool()
 136 | execute_ipython_tool = ExecuteIpythonTool()
 137 | list_files_tool = ListFilesTool()
 138 | list_kernel_tool = ListKernelsTool()
 139 | 
 140 | 
 141 | ###############################################################################
 142 | 
 143 | 
 144 | class ServerContext:
 145 |     """Singleton to cache server mode and context managers."""
 146 |     _instance = None
 147 |     _mode = None
 148 |     _contents_manager = None
 149 |     _kernel_manager = None
 150 |     _kernel_spec_manager = None
 151 |     _session_manager = None
 152 |     _server_client = None
 153 |     _kernel_client = None
 154 |     _initialized = False
 155 |     
 156 |     @classmethod
 157 |     def get_instance(cls):
 158 |         if cls._instance is None:
 159 |             cls._instance = cls()
 160 |         return cls._instance
 161 |     
 162 |     @classmethod
 163 |     def reset(cls):
 164 |         """Reset the singleton instance. Use this when config changes."""
 165 |         if cls._instance is not None:
 166 |             cls._instance._initialized = False
 167 |             cls._instance._mode = None
 168 |             cls._instance._contents_manager = None
 169 |             cls._instance._kernel_manager = None
 170 |             cls._instance._kernel_spec_manager = None
 171 |             cls._instance._session_manager = None
 172 |             cls._instance._server_client = None
 173 |             cls._instance._kernel_client = None
 174 |     
 175 |     def initialize(self):
 176 |         """Initialize context once."""
 177 |         if self._initialized:
 178 |             return
 179 |         
 180 |         try:
 181 |             from jupyter_mcp_server.jupyter_extension.context import get_server_context
 182 |             context = get_server_context()
 183 |             
 184 |             if context.is_local_document() and context.get_contents_manager() is not None:
 185 |                 self._mode = ServerMode.JUPYTER_SERVER
 186 |                 self._contents_manager = context.get_contents_manager()
 187 |                 self._kernel_manager = context.get_kernel_manager()
 188 |                 self._kernel_spec_manager = context.get_kernel_spec_manager() if hasattr(context, 'get_kernel_spec_manager') else None
 189 |                 self._session_manager = context.get_session_manager() if hasattr(context, 'get_session_manager') else None
 190 |             else:
 191 |                 self._mode = ServerMode.MCP_SERVER
 192 |                 # Initialize HTTP clients for MCP_SERVER mode
 193 |                 config = get_config()
 194 |                 
 195 |                 # Validate that runtime_url is set and not None/empty
 196 |                 # Note: String "None" values should have been normalized by start_command()
 197 |                 runtime_url = config.runtime_url
 198 |                 if not runtime_url or runtime_url in ("None", "none", "null", ""):
 199 |                     raise ValueError(
 200 |                         f"runtime_url is not configured (current value: {repr(runtime_url)}). "
 201 |                         "Please check:\n"
 202 |                         "1. RUNTIME_URL environment variable is set correctly (not the string 'None')\n"
 203 |                         "2. --runtime-url argument is provided when starting the server\n"
 204 |                         "3. The MCP client configuration passes runtime_url correctly"
 205 |                     )
 206 |                 
 207 |                 logger.info(f"Initializing MCP_SERVER mode with runtime_url: {runtime_url}")
 208 |                 self._server_client = JupyterServerClient(base_url=runtime_url, token=config.runtime_token)
 209 |                 # kernel_client will be created lazily when needed
 210 |         except (ImportError, Exception) as e:
 211 |             # If not in Jupyter context, use MCP_SERVER mode
 212 |             if not isinstance(e, ValueError):
 213 |                 self._mode = ServerMode.MCP_SERVER
 214 |                 # Initialize HTTP clients for MCP_SERVER mode
 215 |                 config = get_config()
 216 |                 
 217 |                 # Validate that runtime_url is set and not None/empty
 218 |                 # Note: String "None" values should have been normalized by start_command()
 219 |                 runtime_url = config.runtime_url
 220 |                 if not runtime_url or runtime_url in ("None", "none", "null", ""):
 221 |                     raise ValueError(
 222 |                         f"runtime_url is not configured (current value: {repr(runtime_url)}). "
 223 |                         "Please check:\n"
 224 |                         "1. RUNTIME_URL environment variable is set correctly (not the string 'None')\n"
 225 |                         "2. --runtime-url argument is provided when starting the server\n"
 226 |                         "3. The MCP client configuration passes runtime_url correctly"
 227 |                     )
 228 |                 
 229 |                 logger.info(f"Initializing MCP_SERVER mode with runtime_url: {runtime_url}")
 230 |                 self._server_client = JupyterServerClient(base_url=runtime_url, token=config.runtime_token)
 231 |             else:
 232 |                 raise
 233 |         
 234 |         self._initialized = True
 235 |         logger.info(f"Server mode initialized: {self._mode}")
 236 |     
 237 |     @property
 238 |     def mode(self):
 239 |         if not self._initialized:
 240 |             self.initialize()
 241 |         return self._mode
 242 |     
 243 |     @property
 244 |     def contents_manager(self):
 245 |         if not self._initialized:
 246 |             self.initialize()
 247 |         return self._contents_manager
 248 |     
 249 |     @property
 250 |     def kernel_manager(self):
 251 |         if not self._initialized:
 252 |             self.initialize()
 253 |         return self._kernel_manager
 254 |     
 255 |     @property
 256 |     def kernel_spec_manager(self):
 257 |         if not self._initialized:
 258 |             self.initialize()
 259 |         return self._kernel_spec_manager
 260 |     
 261 |     @property
 262 |     def session_manager(self):
 263 |         if not self._initialized:
 264 |             self.initialize()
 265 |         return self._session_manager
 266 |     
 267 |     @property
 268 |     def server_client(self):
 269 |         if not self._initialized:
 270 |             self.initialize()
 271 |         return self._server_client
 272 |     
 273 |     @property
 274 |     def kernel_client(self):
 275 |         if not self._initialized:
 276 |             self.initialize()
 277 |         return self._kernel_client
 278 | 
 279 | 
 280 | # Initialize server context singleton
 281 | server_context = ServerContext.get_instance()
 282 | 
 283 | 
 284 | ###############################################################################
 285 | 
 286 | 
 287 | def __create_kernel() -> KernelClient:
 288 |     """Create a new kernel instance using current configuration."""
 289 |     config = get_config()
 290 |     return create_kernel(config, logger)
 291 | 
 292 | 
 293 | def __start_kernel():
 294 |     """Start the Jupyter kernel with error handling (for backward compatibility)."""
 295 |     config = get_config()
 296 |     start_kernel(notebook_manager, config, logger)
 297 | 
 298 | 
 299 | async def __auto_enroll_document():
 300 |     """Wrapper for auto_enroll_document that uses server context."""
 301 |     await auto_enroll_document(
 302 |         config=get_config(),
 303 |         notebook_manager=notebook_manager,
 304 |         use_notebook_tool=use_notebook_tool,
 305 |         server_context=server_context,
 306 |     )
 307 | 
 308 | 
 309 | def __ensure_kernel_alive() -> KernelClient:
 310 |     """Ensure kernel is running, restart if needed."""
 311 |     current_notebook = notebook_manager.get_current_notebook() or "default"
 312 |     return ensure_kernel_alive(notebook_manager, current_notebook, __create_kernel)
 313 | 
 314 | 
 315 | async def __execute_cell_with_timeout(notebook, cell_index, kernel, timeout_seconds=300):
 316 |     """Execute a cell with timeout and real-time output sync."""
 317 |     return await execute_cell_with_timeout(notebook, cell_index, kernel, timeout_seconds, logger)
 318 | 
 319 | 
 320 | async def __execute_cell_with_forced_sync(notebook, cell_index, kernel, timeout_seconds=300):
 321 |     """Execute cell with forced real-time synchronization."""
 322 |     return await execute_cell_with_forced_sync(notebook, cell_index, kernel, timeout_seconds, logger)
 323 | 
 324 | 
 325 | def __is_kernel_busy(kernel):
 326 |     """Check if kernel is currently executing something."""
 327 |     return is_kernel_busy(kernel)
 328 | 
 329 | 
 330 | async def __wait_for_kernel_idle(kernel, max_wait_seconds=60):
 331 |     """Wait for kernel to become idle before proceeding."""
 332 |     return await wait_for_kernel_idle(kernel, logger, max_wait_seconds)
 333 | 
 334 | 
 335 | async def __safe_notebook_operation(operation_func, max_retries=3):
 336 |     """Safely execute notebook operations with connection recovery."""
 337 |     return await safe_notebook_operation(operation_func, logger, max_retries)
 338 | 
 339 | 
 340 | def _list_files_recursively(server_client, current_path="", current_depth=0, files=None, max_depth=3):
 341 |     """Recursively list all files and directories in the Jupyter server."""
 342 |     return list_files_recursively(server_client, current_path, current_depth, files, max_depth)
 343 | 
 344 | 
 345 | ###############################################################################
 346 | # Custom Routes.
 347 | 
 348 | 
 349 | @mcp.custom_route("/api/connect", ["PUT"])
 350 | async def connect(request: Request):
 351 |     """Connect to a document and a runtime from the Jupyter MCP server."""
 352 | 
 353 |     data = await request.json()
 354 |     
 355 |     # Log the received data for diagnostics
 356 |     # Note: set_config() will automatically normalize string "None" values
 357 |     logger.info(
 358 |         f"Connect endpoint received - runtime_url: {repr(data.get('runtime_url'))}, "
 359 |         f"document_url: {repr(data.get('document_url'))}, "
 360 |         f"provider: {data.get('provider')}"
 361 |     )
 362 | 
 363 |     document_runtime = DocumentRuntime(**data)
 364 | 
 365 |     # Clean up existing default notebook if any
 366 |     if "default" in notebook_manager:
 367 |         try:
 368 |             notebook_manager.remove_notebook("default")
 369 |         except Exception as e:
 370 |             logger.warning(f"Error stopping existing notebook during connect: {e}")
 371 | 
 372 |     # Update configuration with new values
 373 |     # String "None" values will be automatically normalized by set_config()
 374 |     set_config(
 375 |         provider=document_runtime.provider,
 376 |         runtime_url=document_runtime.runtime_url,
 377 |         runtime_id=document_runtime.runtime_id,
 378 |         runtime_token=document_runtime.runtime_token,
 379 |         document_url=document_runtime.document_url,
 380 |         document_id=document_runtime.document_id,
 381 |         document_token=document_runtime.document_token
 382 |     )
 383 |     
 384 |     # Reset ServerContext to pick up new configuration
 385 |     ServerContext.reset()
 386 | 
 387 |     try:
 388 |         __start_kernel()
 389 |         return JSONResponse({"success": True})
 390 |     except Exception as e:
 391 |         logger.error(f"Failed to connect: {e}")
 392 |         return JSONResponse({"success": False, "error": str(e)}, status_code=500)
 393 | 
 394 | 
 395 | @mcp.custom_route("/api/stop", ["DELETE"])
 396 | async def stop(request: Request):
 397 |     try:
 398 |         current_notebook = notebook_manager.get_current_notebook() or "default"
 399 |         if current_notebook in notebook_manager:
 400 |             notebook_manager.remove_notebook(current_notebook)
 401 |         return JSONResponse({"success": True})
 402 |     except Exception as e:
 403 |         logger.error(f"Error stopping notebook: {e}")
 404 |         return JSONResponse({"success": False, "error": str(e)}, status_code=500)
 405 | 
 406 | 
 407 | @mcp.custom_route("/api/healthz", ["GET"])
 408 | async def health_check(request: Request):
 409 |     """Custom health check endpoint"""
 410 |     kernel_status = "unknown"
 411 |     try:
 412 |         current_notebook = notebook_manager.get_current_notebook() or "default"
 413 |         kernel = notebook_manager.get_kernel(current_notebook)
 414 |         if kernel:
 415 |             kernel_status = "alive" if hasattr(kernel, 'is_alive') and kernel.is_alive() else "dead"
 416 |         else:
 417 |             kernel_status = "not_initialized"
 418 |     except Exception:
 419 |         kernel_status = "error"
 420 |     return JSONResponse(
 421 |         {
 422 |             "success": True,
 423 |             "service": "jupyter-mcp-server",
 424 |             "message": "Jupyter MCP Server is running.",
 425 |             "status": "healthy",
 426 |             "kernel_status": kernel_status,
 427 |         }
 428 |     )
 429 | 
 430 | 
 431 | ###############################################################################
 432 | # Tools.
 433 | ###############################################################################
 434 | 
 435 | ###############################################################################
 436 | # Multi-Notebook Management Tools.
 437 | 
 438 | 
 439 | @mcp.tool()
 440 | async def use_notebook(
 441 |     notebook_name: str,
 442 |     notebook_path: Optional[str] = None,
 443 |     mode: Literal["connect", "create"] = "connect",
 444 |     kernel_id: Optional[str] = None,
 445 | ) -> str:
 446 |     """Use a notebook file (connect to existing or create new, or switch to already-connected notebook).
 447 |     
 448 |     Args:
 449 |         notebook_name: Unique identifier for the notebook
 450 |         notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb"). 
 451 |                       Optional - if not provided, switches to an already-connected notebook with the given name.
 452 |         mode: "connect" to connect to existing, "create" to create new
 453 |         kernel_id: Specific kernel ID to use (optional, will create new if not provided)
 454 |         
 455 |     Returns:
 456 |         str: Success message with notebook information
 457 |     """
 458 |     config = get_config()
 459 |     return await __safe_notebook_operation(
 460 |         lambda: use_notebook_tool.execute(
 461 |             mode=server_context.mode,
 462 |             server_client=server_context.server_client,
 463 |             notebook_name=notebook_name,
 464 |             notebook_path=notebook_path,
 465 |             use_mode=mode,
 466 |             kernel_id=kernel_id,
 467 |             ensure_kernel_alive_fn=__ensure_kernel_alive,
 468 |             contents_manager=server_context.contents_manager,
 469 |             kernel_manager=server_context.kernel_manager,
 470 |             session_manager=server_context.session_manager,
 471 |             notebook_manager=notebook_manager,
 472 |             runtime_url=config.runtime_url if config.runtime_url != "local" else None,
 473 |             runtime_token=config.runtime_token,
 474 |         )
 475 |     )
 476 | 
 477 | 
 478 | @mcp.tool()
 479 | async def list_notebooks() -> str:
 480 |     """List all notebooks in the Jupyter server (including subdirectories) and show which ones are managed.
 481 |     
 482 |     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.
 483 |     
 484 |     Returns:
 485 |         str: TSV formatted table with notebook information including management status
 486 |     """
 487 |     return await list_notebook_tool.execute(
 488 |         mode=server_context.mode,
 489 |         server_client=server_context.server_client,
 490 |         contents_manager=server_context.contents_manager,
 491 |         kernel_manager=server_context.kernel_manager,
 492 |         notebook_manager=notebook_manager,
 493 |     )
 494 | 
 495 | 
 496 | @mcp.tool()
 497 | async def restart_notebook(notebook_name: str) -> str:
 498 |     """Restart the kernel for a specific notebook.
 499 |     
 500 |     Args:
 501 |         notebook_name: Notebook identifier to restart
 502 |         
 503 |     Returns:
 504 |         str: Success message
 505 |     """
 506 |     return await restart_notebook_tool.execute(
 507 |         mode=server_context.mode,
 508 |         notebook_name=notebook_name,
 509 |         notebook_manager=notebook_manager,
 510 |         kernel_manager=server_context.kernel_manager,
 511 |     )
 512 | 
 513 | 
 514 | @mcp.tool()
 515 | async def unuse_notebook(notebook_name: str) -> str:
 516 |     """Unuse from a specific notebook and release its resources.
 517 |     
 518 |     Args:
 519 |         notebook_name: Notebook identifier to disconnect
 520 |         
 521 |     Returns:
 522 |         str: Success message
 523 |     """
 524 |     return await unuse_notebook_tool.execute(
 525 |         mode=server_context.mode,
 526 |         notebook_name=notebook_name,
 527 |         notebook_manager=notebook_manager,
 528 |         kernel_manager=server_context.kernel_manager,
 529 |     )
 530 | 
 531 | 
 532 | ###############################################################################
 533 | # Cell Tools.
 534 | 
 535 | @mcp.tool()
 536 | async def insert_cell(
 537 |     cell_index: int,
 538 |     cell_type: Literal["code", "markdown"],
 539 |     cell_source: str,
 540 | ) -> str:
 541 |     """Insert a cell to specified position.
 542 | 
 543 |     Args:
 544 |         cell_index: target index for insertion (0-based). Use -1 to append at end.
 545 |         cell_type: Type of cell to insert ("code" or "markdown")
 546 |         cell_source: Source content for the cell
 547 | 
 548 |     Returns:
 549 |         str: Success message and the structure of its surrounding cells (up to 5 cells above and 5 cells below)
 550 |     """
 551 |     return await __safe_notebook_operation(
 552 |         lambda: insert_cell_tool.execute(
 553 |             mode=server_context.mode,
 554 |             server_client=server_context.server_client,
 555 |             contents_manager=server_context.contents_manager,
 556 |             kernel_manager=server_context.kernel_manager,
 557 |             notebook_manager=notebook_manager,
 558 |             cell_index=cell_index,
 559 |             cell_source=cell_source,
 560 |             cell_type=cell_type,
 561 |         )
 562 |     )
 563 | 
 564 | 
 565 | @mcp.tool()
 566 | async def insert_execute_code_cell(cell_index: int, cell_source: str) -> list[Union[str, ImageContent]]:
 567 |     """Insert and execute a code cell in a Jupyter notebook.
 568 | 
 569 |     Args:
 570 |         cell_index: Index of the cell to insert (0-based). Use -1 to append at end and execute.
 571 |         cell_source: Code source
 572 | 
 573 |     Returns:
 574 |         list[Union[str, ImageContent]]: List of outputs from the executed cell
 575 |     """
 576 |     return await __safe_notebook_operation(
 577 |         lambda: insert_execute_code_cell_tool.execute(
 578 |             mode=server_context.mode,
 579 |             server_client=server_context.server_client,
 580 |             contents_manager=server_context.contents_manager,
 581 |             kernel_manager=server_context.kernel_manager,
 582 |             notebook_manager=notebook_manager,
 583 |             cell_index=cell_index,
 584 |             cell_source=cell_source,
 585 |             ensure_kernel_alive=__ensure_kernel_alive,
 586 |         )
 587 |     )
 588 | 
 589 | 
 590 | @mcp.tool()
 591 | async def overwrite_cell_source(cell_index: int, cell_source: str) -> str:
 592 |     """Overwrite the source of an existing cell.
 593 |        Note this does not execute the modified cell by itself.
 594 | 
 595 |     Args:
 596 |         cell_index: Index of the cell to overwrite (0-based)
 597 |         cell_source: New cell source - must match existing cell type
 598 | 
 599 |     Returns:
 600 |         str: Success message with diff showing changes made
 601 |     """
 602 |     return await __safe_notebook_operation(
 603 |         lambda: overwrite_cell_source_tool.execute(
 604 |             mode=server_context.mode,
 605 |             server_client=server_context.server_client,
 606 |             contents_manager=server_context.contents_manager,
 607 |             kernel_manager=server_context.kernel_manager,
 608 |             notebook_manager=notebook_manager,
 609 |             cell_index=cell_index,
 610 |             cell_source=cell_source,
 611 |         )
 612 |     )
 613 | 
 614 | @mcp.tool()
 615 | async def execute_cell(cell_index: int, timeout_seconds: int = 300, stream: bool = False, progress_interval: int = 5) -> list[Union[str, ImageContent]]:
 616 |     """Execute a cell with configurable timeout and optional streaming progress updates.
 617 | 
 618 |     Args:
 619 |         cell_index: Index of the cell to execute (0-based)
 620 |         timeout_seconds: Maximum time to wait for execution (default: 300s)
 621 |         stream: Enable streaming progress updates for long-running cells (default: False)
 622 |         progress_interval: Seconds between progress updates when stream=True (default: 5s)
 623 |     Returns:
 624 |         list[Union[str, ImageContent]]: List of outputs from the executed cell
 625 |     """
 626 |     return await __safe_notebook_operation(
 627 |         lambda: execute_cell_tool.execute(
 628 |             mode=server_context.mode,
 629 |             server_client=server_context.server_client,
 630 |             contents_manager=server_context.contents_manager,
 631 |             kernel_manager=server_context.kernel_manager,
 632 |             notebook_manager=notebook_manager,
 633 |             cell_index=cell_index,
 634 |             timeout_seconds=timeout_seconds,
 635 |             stream=stream,
 636 |             progress_interval=progress_interval,
 637 |             ensure_kernel_alive_fn=__ensure_kernel_alive,
 638 |             wait_for_kernel_idle_fn=__wait_for_kernel_idle,
 639 |             safe_extract_outputs_fn=safe_extract_outputs,
 640 |             execute_cell_with_forced_sync_fn=__execute_cell_with_forced_sync,
 641 |             extract_output_fn=extract_output,
 642 |         ),
 643 |         max_retries=1
 644 |     )
 645 | 
 646 | 
 647 | @mcp.tool()
 648 | async def read_cells() -> list[dict[str, Union[str, int, list[Union[str, ImageContent]]]]]:
 649 |     """Read all cells from the Jupyter notebook.
 650 |     Returns:
 651 |         list[dict]: List of cell information including index, type, source,
 652 |                     and outputs (for code cells)
 653 |     """
 654 |     return await __safe_notebook_operation(
 655 |         lambda: read_cells_tool.execute(
 656 |             mode=server_context.mode,
 657 |             server_client=server_context.server_client,
 658 |             contents_manager=server_context.contents_manager,
 659 |             notebook_manager=notebook_manager,
 660 |         )
 661 |     )
 662 | 
 663 | 
 664 | @mcp.tool()
 665 | async def list_cells() -> str:
 666 |     """List the basic information of all cells in the notebook.
 667 |     
 668 |     Returns a formatted table showing the index, type, execution count (for code cells),
 669 |     and first line of each cell. This provides a quick overview of the notebook structure
 670 |     and is useful for locating specific cells for operations like delete or insert.
 671 |     
 672 |     Returns:
 673 |         str: Formatted table with cell information (Index, Type, Count, First Line)
 674 |     """
 675 |     return await __safe_notebook_operation(
 676 |         lambda: list_cells_tool.execute(
 677 |             mode=server_context.mode,
 678 |             server_client=server_context.server_client,
 679 |             contents_manager=server_context.contents_manager,
 680 |             notebook_manager=notebook_manager,
 681 |         )
 682 |     )
 683 | 
 684 | 
 685 | @mcp.tool()
 686 | async def read_cell(cell_index: int) -> dict[str, Union[str, int, list[Union[str, ImageContent]]]]:
 687 |     """Read a specific cell from the Jupyter notebook.
 688 |     Args:
 689 |         cell_index: Index of the cell to read (0-based)
 690 |     Returns:
 691 |         dict: Cell information including index, type, source, and outputs (for code cells)
 692 |     """
 693 |     return await __safe_notebook_operation(
 694 |         lambda: read_cell_tool.execute(
 695 |             mode=server_context.mode,
 696 |             server_client=server_context.server_client,
 697 |             contents_manager=server_context.contents_manager,
 698 |             notebook_manager=notebook_manager,
 699 |             cell_index=cell_index,
 700 |         )
 701 |     )
 702 | 
 703 | @mcp.tool()
 704 | async def delete_cell(cell_index: int) -> str:
 705 |     """Delete a specific cell from the Jupyter notebook.
 706 |     Args:
 707 |         cell_index: Index of the cell to delete (0-based)
 708 |     Returns:
 709 |         str: Success message
 710 |     """
 711 |     return await __safe_notebook_operation(
 712 |         lambda: delete_cell_tool.execute(
 713 |             mode=server_context.mode,
 714 |             server_client=server_context.server_client,
 715 |             contents_manager=server_context.contents_manager,
 716 |             kernel_manager=server_context.kernel_manager,
 717 |             notebook_manager=notebook_manager,
 718 |             cell_index=cell_index,
 719 |         )
 720 |     )
 721 | 
 722 | 
 723 | @mcp.tool()
 724 | async def execute_ipython(code: str, timeout: int = 60) -> list[Union[str, ImageContent]]:
 725 |     """Execute IPython code directly in the kernel on the current active notebook.
 726 |     
 727 |     This powerful tool supports:
 728 |     1. Magic commands (e.g., %timeit, %who, %load, %run, %matplotlib)
 729 |     2. Shell commands (e.g., !pip install, !ls, !cat)
 730 |     3. Python code (e.g., print(df.head()), df.info())
 731 |     
 732 |     Use cases:
 733 |     - Performance profiling and debugging
 734 |     - Environment exploration and package management
 735 |     - Variable inspection and data analysis
 736 |     - File system operations on Jupyter server
 737 |     - Temporary calculations and quick tests
 738 | 
 739 |     Args:
 740 |         code: IPython code to execute (supports magic commands, shell commands with !, and Python code)
 741 |         timeout: Execution timeout in seconds (default: 60s)
 742 |     Returns:
 743 |         List of outputs from the executed code
 744 |     """
 745 |     # Get kernel_id for JUPYTER_SERVER mode
 746 |     # Let the tool handle getting kernel_id via get_current_notebook_context()
 747 |     kernel_id = None
 748 |     if server_context.mode == ServerMode.JUPYTER_SERVER:
 749 |         current_notebook = notebook_manager.get_current_notebook() or "default"
 750 |         kernel_id = notebook_manager.get_kernel_id(current_notebook)
 751 |         # Note: kernel_id might be None here if notebook not in manager,
 752 |         # but the tool will fall back to config values via get_current_notebook_context()
 753 |     
 754 |     return await __safe_notebook_operation(
 755 |         lambda: execute_ipython_tool.execute(
 756 |             mode=server_context.mode,
 757 |             server_client=server_context.server_client,
 758 |             kernel_manager=server_context.kernel_manager,
 759 |             notebook_manager=notebook_manager,
 760 |             code=code,
 761 |             timeout=timeout,
 762 |             kernel_id=kernel_id,
 763 |             ensure_kernel_alive_fn=__ensure_kernel_alive,
 764 |             wait_for_kernel_idle_fn=__wait_for_kernel_idle,
 765 |             safe_extract_outputs_fn=safe_extract_outputs,
 766 |         ),
 767 |         max_retries=1
 768 |     )
 769 | 
 770 | 
 771 | @mcp.tool()
 772 | async def list_files(path: str = "", max_depth: int = 3) -> str:
 773 |     """List all files and directories in the Jupyter server's file system.
 774 |     
 775 |     This tool recursively lists files and directories from the Jupyter server's content API,
 776 |     showing the complete file structure including notebooks, data files, scripts, and directories.
 777 |     
 778 |     Args:
 779 |         path: The starting path to list from (empty string means root directory)
 780 |         max_depth: Maximum depth to recurse into subdirectories (default: 3)
 781 |         
 782 |     Returns:
 783 |         str: Tab-separated table with columns: Path, Type, Size, Last_Modified
 784 |     """
 785 |     return await __safe_notebook_operation(
 786 |         lambda: list_files_tool.execute(
 787 |             mode=server_context.mode,
 788 |             server_client=server_context.server_client,
 789 |             contents_manager=server_context.contents_manager,
 790 |             path=path,
 791 |             max_depth=max_depth,
 792 |             list_files_recursively_fn=_list_files_recursively,
 793 |         )
 794 |     )
 795 | 
 796 | 
 797 | @mcp.tool()
 798 | async def list_kernels() -> str:
 799 |     """List all available kernels in the Jupyter server.
 800 |     
 801 |     This tool shows all running and available kernel sessions on the Jupyter server,
 802 |     including their IDs, names, states, connection information, and kernel specifications.
 803 |     Useful for monitoring kernel resources and identifying specific kernels for connection.
 804 |     
 805 |     Returns:
 806 |         str: Tab-separated table with columns: ID, Name, Display_Name, Language, State, Connections, Last_Activity, Environment
 807 |     """
 808 |     return await __safe_notebook_operation(
 809 |         lambda: list_kernel_tool.execute(
 810 |             mode=server_context.mode,
 811 |             server_client=server_context.server_client,
 812 |             kernel_manager=server_context.kernel_manager,
 813 |             kernel_spec_manager=server_context.kernel_spec_manager,
 814 |         )
 815 |     )
 816 | 
 817 | 
 818 | @mcp.tool()
 819 | async def assign_kernel_to_notebook(
 820 |     notebook_path: str,
 821 |     kernel_id: str,
 822 |     session_name: str = None
 823 | ) -> str:
 824 |     """Assign a kernel to a notebook by creating a Jupyter session.
 825 |     
 826 |     This creates a Jupyter server session that connects a notebook file to a kernel,
 827 |     enabling code execution in the notebook. Sessions are the mechanism Jupyter uses
 828 |     to maintain the relationship between notebooks and their kernels.
 829 |     
 830 |     Args:
 831 |         notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb")
 832 |         kernel_id: ID of the kernel to assign to the notebook
 833 |         session_name: Optional name for the session (defaults to notebook path)
 834 |     
 835 |     Returns:
 836 |         str: Success message with session information including session ID
 837 |     """
 838 |     return await __safe_notebook_operation(
 839 |         lambda: assign_kernel_to_notebook_tool.execute(
 840 |             mode=server_context.mode,
 841 |             server_client=server_context.server_client,
 842 |             contents_manager=server_context.contents_manager,
 843 |             session_manager=server_context.session_manager,
 844 |             kernel_manager=server_context.kernel_manager,
 845 |             notebook_path=notebook_path,
 846 |             kernel_id=kernel_id,
 847 |             session_name=session_name,
 848 |         )
 849 |     )
 850 | 
 851 | 
 852 | ###############################################################################
 853 | # Helper Functions for Extension.
 854 | 
 855 | 
 856 | async def get_registered_tools():
 857 |     """
 858 |     Get list of all registered MCP tools with their metadata.
 859 |     
 860 |     This function is used by the Jupyter extension to dynamically expose
 861 |     the tool registry without hardcoding tool names and parameters.
 862 |     
 863 |     Returns:
 864 |         list: List of tool dictionaries with name, description, and inputSchema
 865 |     """
 866 |     # Use FastMCP's list_tools method which returns Tool objects
 867 |     tools_list = await mcp.list_tools()
 868 |     
 869 |     tools = []
 870 |     for tool in tools_list:
 871 |         tool_dict = {
 872 |             "name": tool.name,
 873 |             "description": tool.description,
 874 |         }
 875 |         
 876 |         # Extract parameter names from inputSchema
 877 |         if hasattr(tool, 'inputSchema') and tool.inputSchema:
 878 |             input_schema = tool.inputSchema
 879 |             if 'properties' in input_schema:
 880 |                 tool_dict["parameters"] = list(input_schema['properties'].keys())
 881 |             else:
 882 |                 tool_dict["parameters"] = []
 883 |             
 884 |             # Include full inputSchema for MCP protocol compatibility
 885 |             tool_dict["inputSchema"] = input_schema
 886 |         else:
 887 |             tool_dict["parameters"] = []
 888 |         
 889 |         tools.append(tool_dict)
 890 |     
 891 |     return tools
 892 | 
 893 | 
 894 | ###############################################################################
 895 | # Commands.
 896 | 
 897 | 
 898 | # Shared options decorator to reduce code duplication
 899 | def _common_options(f):
 900 |     """Decorator that adds common start options to a command."""
 901 |     options = [
 902 |         click.option(
 903 |             "--provider",
 904 |             envvar="PROVIDER",
 905 |             type=click.Choice(["jupyter", "datalayer"]),
 906 |             default="jupyter",
 907 |             help="The provider to use for the document and runtime. Defaults to 'jupyter'.",
 908 |         ),
 909 |         click.option(
 910 |             "--runtime-url",
 911 |             envvar="RUNTIME_URL",
 912 |             type=click.STRING,
 913 |             default="http://localhost:8888",
 914 |             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.",
 915 |         ),
 916 |         click.option(
 917 |             "--runtime-id",
 918 |             envvar="RUNTIME_ID",
 919 |             type=click.STRING,
 920 |             default=None,
 921 |             help="The kernel ID to use. If not provided, a new kernel should be started.",
 922 |         ),
 923 |         click.option(
 924 |             "--runtime-token",
 925 |             envvar="RUNTIME_TOKEN",
 926 |             type=click.STRING,
 927 |             default=None,
 928 |             help="The runtime token to use for authentication with the provider. If not provided, the provider should accept anonymous requests.",
 929 |         ),
 930 |         click.option(
 931 |             "--document-url",
 932 |             envvar="DOCUMENT_URL",
 933 |             type=click.STRING,
 934 |             default="http://localhost:8888",
 935 |             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.",
 936 |         ),
 937 |         click.option(
 938 |             "--document-id",
 939 |             envvar="DOCUMENT_ID",
 940 |             type=click.STRING,
 941 |             default=None,
 942 |             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.",
 943 |         ),
 944 |         click.option(
 945 |             "--document-token",
 946 |             envvar="DOCUMENT_TOKEN",
 947 |             type=click.STRING,
 948 |             default=None,
 949 |             help="The document token to use for authentication with the provider. If not provided, the provider should accept anonymous requests.",
 950 |         )
 951 |     ]
 952 |     # Apply decorators in reverse order
 953 |     for option in reversed(options):
 954 |         f = option(f)
 955 |     return f
 956 | 
 957 | def _do_start(
 958 |     transport: str,
 959 |     start_new_runtime: bool,
 960 |     runtime_url: str,
 961 |     runtime_id: str,
 962 |     runtime_token: str,
 963 |     document_url: str,
 964 |     document_id: str,
 965 |     document_token: str,
 966 |     port: int,
 967 |     provider: str,
 968 | ):
 969 |     """Internal function to execute the start logic."""
 970 |     
 971 |     # Log the received configuration for diagnostics
 972 |     # Note: set_config() will automatically normalize string "None" values
 973 |     logger.info(
 974 |         f"Start command received - runtime_url: {repr(runtime_url)}, "
 975 |         f"document_url: {repr(document_url)}, provider: {provider}, "
 976 |         f"transport: {transport}"
 977 |     )
 978 | 
 979 |     # Set configuration using the singleton
 980 |     # String "None" values will be automatically normalized by set_config()
 981 |     config = set_config(
 982 |         transport=transport,
 983 |         provider=provider,
 984 |         runtime_url=runtime_url,
 985 |         start_new_runtime=start_new_runtime,
 986 |         runtime_id=runtime_id,
 987 |         runtime_token=runtime_token,
 988 |         document_url=document_url,
 989 |         document_id=document_id,
 990 |         document_token=document_token,
 991 |         port=port
 992 |     )
 993 |     
 994 |     # Reset ServerContext to pick up new configuration
 995 |     ServerContext.reset()
 996 | 
 997 |     # Determine startup behavior based on configuration
 998 |     if config.document_id:
 999 |         # If document_id is provided, auto-enroll the notebook
1000 |         # Kernel creation depends on start_new_runtime and runtime_id flags
1001 |         try:
1002 |             import asyncio
1003 |             # Run the async enrollment in the event loop
1004 |             asyncio.run(__auto_enroll_document())
1005 |         except Exception as e:
1006 |             logger.error(f"Failed to auto-enroll document '{config.document_id}': {e}")
1007 |             # Fallback to legacy kernel-only mode if enrollment fails
1008 |             if config.start_new_runtime or config.runtime_id:
1009 |                 try:
1010 |                     __start_kernel()
1011 |                 except Exception as e2:
1012 |                     logger.error(f"Failed to start kernel on startup: {e2}")
1013 |     elif config.start_new_runtime or config.runtime_id:
1014 |         # If no document_id but start_new_runtime/runtime_id is set, just create kernel
1015 |         # This is for backward compatibility - kernel without managed notebook
1016 |         try:
1017 |             __start_kernel()
1018 |         except Exception as e:
1019 |             logger.error(f"Failed to start kernel on startup: {e}")
1020 |     # else: No startup action - user must manually enroll notebooks or create kernels
1021 | 
1022 |     logger.info(f"Starting Jupyter MCP Server with transport: {transport}")
1023 | 
1024 |     if transport == "stdio":
1025 |         mcp.run(transport="stdio")
1026 |     elif transport == "streamable-http":
1027 |         uvicorn.run(mcp.streamable_http_app, host="0.0.0.0", port=port)  # noqa: S104
1028 |     else:
1029 |         raise Exception("Transport should be `stdio` or `streamable-http`.")
1030 | 
1031 | 
1032 | @click.group(invoke_without_command=True)
1033 | @_common_options
1034 | @click.option(
1035 |     "--transport",
1036 |     envvar="TRANSPORT",
1037 |     type=click.Choice(["stdio", "streamable-http"]),
1038 |     default="stdio",
1039 |     help="The transport to use for the MCP server. Defaults to 'stdio'.",
1040 | )
1041 | @click.option(
1042 |     "--start-new-runtime",
1043 |     envvar="START_NEW_RUNTIME",
1044 |     type=click.BOOL,
1045 |     default=True,
1046 |     help="Start a new runtime or use an existing one.",
1047 | )
1048 | @click.option(
1049 |     "--port",
1050 |     envvar="PORT",
1051 |     type=click.INT,
1052 |     default=4040,
1053 |     help="The port to use for the Streamable HTTP transport. Ignored for stdio transport.",
1054 | )
1055 | @click.pass_context
1056 | def server(
1057 |     ctx,
1058 |     transport: str,
1059 |     start_new_runtime: bool,
1060 |     runtime_url: str,
1061 |     runtime_id: str,
1062 |     runtime_token: str,
1063 |     document_url: str,
1064 |     document_id: str,
1065 |     document_token: str,
1066 |     port: int,
1067 |     provider: str,
1068 | ):
1069 |     """Manages Jupyter MCP Server.
1070 |     
1071 |     When invoked without subcommands, starts the MCP server directly.
1072 |     This allows for quick startup with: uvx jupyter-mcp-server
1073 |     
1074 |     Subcommands (start, connect, stop) are still available for advanced use cases.
1075 |     """
1076 |     # If a subcommand is invoked, let it handle the execution
1077 |     if ctx.invoked_subcommand is not None:
1078 |         return
1079 |     
1080 |     # No subcommand provided - execute the default start behavior
1081 |     _do_start(
1082 |         transport=transport,
1083 |         start_new_runtime=start_new_runtime,
1084 |         runtime_url=runtime_url,
1085 |         runtime_id=runtime_id,
1086 |         runtime_token=runtime_token,
1087 |         document_url=document_url,
1088 |         document_id=document_id,
1089 |         document_token=document_token,
1090 |         port=port,
1091 |         provider=provider,
1092 |     )
1093 | 
1094 | 
1095 | @server.command("connect")
1096 | @_common_options
1097 | @click.option(
1098 |     "--jupyter-mcp-server-url",
1099 |     envvar="JUPYTER_MCP_SERVER_URL",
1100 |     type=click.STRING,
1101 |     default="http://localhost:4040",
1102 |     help="The URL of the Jupyter MCP Server to connect to. Defaults to 'http://localhost:4040'.",
1103 | )
1104 | def connect_command(
1105 |     jupyter_mcp_server_url: str,
1106 |     runtime_url: str,
1107 |     runtime_id: str,
1108 |     runtime_token: str,
1109 |     document_url: str,
1110 |     document_id: str,
1111 |     document_token: str,
1112 |     provider: str,
1113 | ):
1114 |     """Command to connect a Jupyter MCP Server to a document and a runtime."""
1115 | 
1116 |     # Set configuration using the singleton
1117 |     set_config(
1118 |         provider=provider,
1119 |         runtime_url=runtime_url,
1120 |         runtime_id=runtime_id,
1121 |         runtime_token=runtime_token,
1122 |         document_url=document_url,
1123 |         document_id=document_id,
1124 |         document_token=document_token
1125 |     )
1126 | 
1127 |     config = get_config()
1128 |     
1129 |     document_runtime = DocumentRuntime(
1130 |         provider=config.provider,
1131 |         runtime_url=config.runtime_url,
1132 |         runtime_id=config.runtime_id,
1133 |         runtime_token=config.runtime_token,
1134 |         document_url=config.document_url,
1135 |         document_id=config.document_id,
1136 |         document_token=config.document_token,
1137 |     )
1138 | 
1139 |     r = httpx.put(
1140 |         f"{jupyter_mcp_server_url}/api/connect",
1141 |         headers={
1142 |             "Content-Type": "application/json",
1143 |             "Accept": "application/json",
1144 |         },
1145 |         content=document_runtime.model_dump_json(),
1146 |     )
1147 |     r.raise_for_status()
1148 | 
1149 | 
1150 | @server.command("stop")
1151 | @click.option(
1152 |     "--jupyter-mcp-server-url",
1153 |     envvar="JUPYTER_MCP_SERVER_URL",
1154 |     type=click.STRING,
1155 |     default="http://localhost:4040",
1156 |     help="The URL of the Jupyter MCP Server to stop. Defaults to 'http://localhost:4040'.",
1157 | )
1158 | def stop_command(jupyter_mcp_server_url: str):
1159 |     r = httpx.delete(
1160 |         f"{jupyter_mcp_server_url}/api/stop",
1161 |     )
1162 |     r.raise_for_status()
1163 | 
1164 | 
1165 | @server.command("start")
1166 | @_common_options
1167 | @click.option(
1168 |     "--transport",
1169 |     envvar="TRANSPORT",
1170 |     type=click.Choice(["stdio", "streamable-http"]),
1171 |     default="stdio",
1172 |     help="The transport to use for the MCP server. Defaults to 'stdio'.",
1173 | )
1174 | @click.option(
1175 |     "--start-new-runtime",
1176 |     envvar="START_NEW_RUNTIME",
1177 |     type=click.BOOL,
1178 |     default=True,
1179 |     help="Start a new runtime or use an existing one.",
1180 | )
1181 | @click.option(
1182 |     "--port",
1183 |     envvar="PORT",
1184 |     type=click.INT,
1185 |     default=4040,
1186 |     help="The port to use for the Streamable HTTP transport. Ignored for stdio transport.",
1187 | )
1188 | def start_command(
1189 |     transport: str,
1190 |     start_new_runtime: bool,
1191 |     runtime_url: str,
1192 |     runtime_id: str,
1193 |     runtime_token: str,
1194 |     document_url: str,
1195 |     document_id: str,
1196 |     document_token: str,
1197 |     port: int,
1198 |     provider: str,
1199 | ):
1200 |     """Start the Jupyter MCP server with a transport."""
1201 |     _do_start(
1202 |         transport=transport,
1203 |         start_new_runtime=start_new_runtime,
1204 |         runtime_url=runtime_url,
1205 |         runtime_id=runtime_id,
1206 |         runtime_token=runtime_token,
1207 |         document_url=document_url,
1208 |         document_id=document_id,
1209 |         document_token=document_token,
1210 |         port=port,
1211 |         provider=provider,
1212 |     )
1213 | 
1214 | 
1215 | ###############################################################################
1216 | # Main.
1217 | 
1218 | 
1219 | if __name__ == "__main__":
1220 |     start_command()
```
Page 5/6FirstPrevNextLast