背景
在我尝试将jupyter kotlin kernel 内嵌到IDEA的JVM后,用jupyter控制IDEA用了很长时间,但除了utils工具类之外IDEA的openapi和公开的方法基本都是为了UI而编写的异步代码,这在自动化过程中获取返回值非常不方便。我预期的是直接复制粘贴官方源码稍微改改就能用,不想对官方代码做大的结构修改。
继IDEA官方编写MCP后,我发现二月份的时候jupyter也有MCP server了。
datalayer在尝试用ai把jupyter自动化,使用自然语言描述数据处理的请求然后生成notebook并执行python代码,我一看这不正好是我想要的么。
如果我人工能操作jupyterlab完成结果,相比调整kotlin脚本的控制流,我只需要糊一点python代码就能代替我手工操作的部分
原理
jupyter mcp server的代码非常的少,我想要的其实是排除mcp的部分,去掉注解改一改直接就能用
本质上是直接调用了jupyter lab的接口获取websocket,通过jupyter lab的collaboration扩展通道操作notebook
过程记录
安装jupyter lab,注意这里新增了jupyter-collaboration
0 1 |
python -m pip install jupyterlab kotlin-jupyter-kernel jupyterlab-lsp jupyter-collaboration git+https://github.com/956237586/run_kotlin_kernel_idea.git@v0.1 git+https://github.com/956237586/jupyter_client.git@v8.4.3 |
实测发现之前的安装命令不好使了,日志报错如下
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - 28669 [Thread-90] DEBUG SocketWrapper - [SHELL] >rcv: msg[0b76731b-0290-4aec-ad16-e36095f16608] {"header":{"date":"2025-05-02T12:03:36.600Z","msg_id":"6000f0b3-e290-46b0-a388-7b960e068dfb","msg_type":"kernel_info_request","session":"0b76731b-0290-4aec-ad16-e36095f16608","username":"","subshell_id":null,"version":"5.2"},"parent_header":null,"metadata":null,"content":{}} 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - 28669 [Thread-90] ERROR SocketWrapper - Exception thrown while processing a message 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - kotlinx.serialization.json.internal.JsonDecodingException: Encountered an unknown key 'subshell_id'. 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys. 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - Current input: .....095f16608","username":"","subshell_id":null,"version":"5.2"} 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.JsonExceptionsKt.UnknownKeyException(JsonExceptions.kt:71) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.JsonTreeDecoder.endStructure(TreeJsonDecoder.kt:276) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageHeader$$serializer.deserialize(message_types.kt:111) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageHeader$$serializer.deserialize(message_types.kt:111) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.internal.NullableSerializer.deserialize(NullableSerializer.kt:30) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:25) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:117) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageDataSerializer.deserialize(message_types.kt:603) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageDataSerializer.deserialize(message_types.kt:537) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:25) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:117) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageKt.toMessage(message.kt:115) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.ProtocolKt.shellMessagesHandler(protocol.kt:243) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$2.invoke(KotlinReplWrapper.kt:220) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$2.invoke(KotlinReplWrapper.kt:218) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.protocol.AbstractJupyterConnection$addMessageCallback$socketCallback$1.invoke(AbstractJupyterConnection.kt:18) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.protocol.AbstractJupyterConnection$addMessageCallback$socketCallback$1.invoke(AbstractJupyterConnection.kt:16) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.protocol.SocketWrapper.runCallbacksOnMessage(SocketWrapper.kt:62) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$3.invoke(KotlinReplWrapper.kt:237) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$3.invoke(KotlinReplWrapper.kt:236) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.kernelServer$lambda$6$socketLoop(KotlinReplWrapper.kt:202) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.kernelServer(KotlinReplWrapper.kt:236) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.kernelServer$default(KotlinReplWrapper.kt:167) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.embedKernelIdea(KotlinReplWrapper.kt:164) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.embedKernelIdea$default(KotlinReplWrapper.kt:145) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.main(KotlinReplWrapper.kt:76) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.makeEmbeddedRepl(KotlinReplWrapper.kt:54) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.service.impl.IdeaPluginAgentServerImpl.lambda$startJupyterKernel$2(IdeaPluginAgentServerImpl.java:281) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at java.base/java.lang.Thread.run(Thread.java:833) |
很明显kernel接到了jupyterlab的请求但反序列化失败了,注意到jupyterlab没指定版本
根据上一次安装记录对比版本号可发现,是最近jupyterlab的更新4.4.0时header新增了一个字段,而kotlin kernel反序列化不接受未定义的字段导致,因此降级到4.3的最后一个版本4.3.6
0 1 |
python -m pip install jupyterlab==4.3.6 kotlin-jupyter-kernel jupyterlab-lsp jupyter-collaboration git+https://github.com/956237586/run_kotlin_kernel_idea.git@v0.1 git+https://github.com/956237586/jupyter_client.git@v8.4.3 |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
------------------------- -------------- anyio 4.9.0 argon2-cffi 23.1.0 argon2-cffi-bindings 21.2.0 arrow 1.3.0 asttokens 3.0.0 async-lru 2.0.5 attrs 25.3.0 babel 2.17.0 beautifulsoup4 4.13.4 bleach 6.2.0 certifi 2025.4.26 cffi 1.17.1 charset-normalizer 3.4.2 colorama 0.4.6 comm 0.2.2 datalayer_pycrdt 0.12.15 debugpy 1.8.14 decorator 5.2.1 defusedxml 0.7.1 executing 2.2.0 fastjsonschema 2.21.1 fqdn 1.5.1 h11 0.16.0 httpcore 1.0.9 httpx 0.28.1 idna 3.10 ipykernel 6.29.5 ipython 9.2.0 ipython_pygments_lexers 1.1.1 isoduration 20.11.0 jedi 0.19.2 Jinja2 3.1.6 json5 0.12.0 jsonpointer 3.0.0 jsonschema 4.23.0 jsonschema-specifications 2025.4.1 jupyter_client 8.4.3 jupyter_core 5.7.2 jupyter_kernel_client 0.6.0 jupyter_nbmodel_client 0.11.3 jupyter_server 2.15.0 jupyter_server_fileid 0.9.3 jupyter_server_terminals 0.5.3 jupyter-collaboration 3.1.2 jupyter-collaboration-ui 1.1.2 jupyter-docprovider 1.1.2 jupyter-events 0.12.0 jupyter-lsp 2.2.5 jupyter-server-ydoc 1.1.2 jupyter-ydoc 3.0.4 jupyterlab 4.3.6 jupyterlab_pygments 0.3.0 jupyterlab_server 2.27.3 jupyterlab-lsp 5.1.0 kotlin-jupyter-kernel 0.12.0.322 MarkupSafe 3.0.2 matplotlib-inline 0.1.7 mistune 3.1.3 nbclient 0.10.2 nbconvert 7.16.6 nbformat 5.10.4 nest-asyncio 1.6.0 notebook_shim 0.2.4 overrides 7.7.0 Package Version packaging 25.0 pandocfilters 1.5.1 parso 0.8.4 pip 25.0.1 platformdirs 4.3.7 prometheus_client 0.21.1 prompt_toolkit 3.0.51 psutil 7.0.0 pure_eval 0.2.3 pycparser 2.22 pycrdt 0.12.15 pycrdt-websocket 0.15.5 Pygments 2.19.1 python-dateutil 2.9.0.post0 python-json-logger 3.3.0 pywin32 310 pywinpty 2.0.15 PyYAML 6.0.2 pyzmq 26.4.0 referencing 0.36.2 requests 2.32.3 rfc3339-validator 0.1.4 rfc3986-validator 0.1.1 rpds-py 0.24.0 run_kotlin_kernel_idea 0.1 Send2Trash 1.8.3 setuptools 80.2.0 six 1.17.0 sniffio 1.3.1 soupsieve 2.7 sqlite-anyio 0.2.3 stack-data 0.6.3 terminado 0.18.1 tinycss2 1.4.0 tornado 6.4.2 traitlets 5.14.3 types-python-dateutil 2.9.0.20241206 typing_extensions 4.13.2 uri-template 1.3.0 urllib3 2.4.0 wcwidth 0.2.13 webcolors 24.11.1 webencodings 0.5.1 websocket-client 1.8.0 websockets 15.0.1 |
降级后测试原来的功能正常
第一次先使用再建一个独立virtualenv防止破坏环境
0 1 |
pip install jupyter-kernel-client==0.6.0 jupyter-nbmodel-client==0.11.3 |
按官方文档安装依赖,修改官方的源码作为测试脚本。因为官方是使用独立进程的python kernel运行脚本,而我是要让一个已经运行了的内嵌kernel执行,所以需要指定下当前的kernel_id复用。否则代码会启动一个新的python kernel执行kotlin脚本就和我的预期不符了
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
import logging import os from jupyter_kernel_client import KernelClient from jupyter_nbmodel_client import ( NbModelClient, get_jupyter_notebook_websocket_url, ) logger = logging.getLogger(__name__) def extract_output(output: dict) -> str: """ Extracts readable output from a Jupyter cell output dictionary. Args: output (dict): The output dictionary from a Jupyter cell. Returns: str: A string representation of the output. """ output_type = output.get("output_type") if output_type == "stream": return output.get("text", "") elif output_type in ["display_data", "execute_result"]: data = output.get("data", {}) if "text/plain" in data: return data["text/plain"] elif "text/html" in data: return "[HTML Output]" elif "image/png" in data: return "[Image Output (PNG)]" else: return f"[{output_type} Data: keys={list(data.keys())}]" elif output_type == "error": return output["traceback"] else: return f"[Unknown output type: {output_type}]" async def add_markdown_cell(cell_content: str) -> str: """Add a markdown cell in a Jupyter notebook. Args: cell_content: Markdown content Returns: str: Success message """ notebook = NbModelClient( get_jupyter_notebook_websocket_url(server_url=SERVER_URL, token=TOKEN, path=NOTEBOOK_PATH) ) await notebook.start() notebook.add_markdown_cell(cell_content) await notebook.stop() return "Jupyter Markdown cell added." async def add_execute_code_cell(cell_content: str, kernel) -> list[str]: """Add and execute a code cell in a Jupyter notebook. Args: cell_content: Code content Returns: list[str]: List of outputs from the executed cell """ notebook = NbModelClient( get_jupyter_notebook_websocket_url(server_url=SERVER_URL, token=TOKEN, path=NOTEBOOK_PATH) ) await notebook.start() cell_index = notebook.add_code_cell(cell_content) if kernel: notebook.execute_cell(cell_index, kernel) ydoc = notebook._doc outputs = ydoc._ycells[cell_index]["outputs"] str_outputs = [extract_output(output) for output in outputs] await notebook.stop() return str_outputs def get_sessions(): import requests # 获取当前Notebook的会话信息 response = requests.get( f"{SERVER_URL}/api/sessions", headers={"Authorization": f"token {TOKEN}"} ) sessions = response.json() # [{"id": "d00ac394-cace-42c1-b56d-a93e7ff9d3a4", "path": "Untitled.ipynb", "name": "Untitled.ipynb", "type": "notebook", "kernel": {"id": "9a1ae062-ed29-49af-862e-c455109283f3", "name": "kotlin_skykoma-agent-idea", "last_activity": "2025-05-02T13:26:07.157812Z", "execution_state": "idle", "connections": 1}, "notebook": {"path": "Untitled.ipynb", "name": "Untitled.ipynb"}}] return sessions def get_kernel_id(): # 提取目标Notebook的Kernel连接信息 kernel_info = None sessions = get_sessions() for session in sessions: if session['notebook']['path'] == NOTEBOOK_PATH: kernel_info = session['kernel'] break # print("Kernel Connection Info:", kernel_info) kernel_id = None if kernel_info: kernel_id = kernel_info["id"] # kernel_name="kotlin_skykoma-agent-idea if kernel_id is None: print(f"get kernel_id error, sessions = {sessions}") return kernel_id NOTEBOOK_PATH = "test/Untitled1.ipynb" SERVER_URL = "http://127.0.0.1:8888" TOKEN = "1234" async def main(code): url = f"{SERVER_URL}/api/sessions" result = await add_markdown_cell("test") print(f"markdown add result = {result}") kernel_id = get_kernel_id() if(kernel_id == None): exit(-1) kernel = KernelClient(server_url=SERVER_URL, token=TOKEN, kernel_id=kernel_id) kernel.start() result = await add_execute_code_cell(code, kernel) kernel.stop() print(f"code execute result = {result}") import asyncio if __name__ == "__main__": code = """ import cn.hylstudio.skykoma.plugin.idea.util.* println("succ") """.lstrip() asyncio.run(main(code)) exit(0) |
既然python文件单独执行是能成功的,也就意味着python kernel也能执行相同的代码,而一个jupyterlab可以同时开启notebook分配不同的kernel。既然如此已经有一个现成的python环境了为什么不直接用呢?因此续尝试合并这个独立的python环境到jupyterlab的python环境
0 1 2 3 4 |
datalayer_pycrdt 0.12.15 jupyter_kernel_client 0.6.0 jupyter_nbmodel_client 0.11.3 websockets 15.0.1 |
合并后的安装命令
0 1 |
python -m pip install jupyterlab==4.3.6 kotlin-jupyter-kernel jupyterlab-lsp jupyter-collaboration jupyter-kernel-client==0.6.0 jupyter-nbmodel-client==0.11.3 git+https://github.com/956237586/run_kotlin_kernel_idea.git@v0.1 git+https://github.com/956237586/jupyter_client.git@v8.4.3 |
然后就可以尝试将脚本放入另一个notebook中了,kernel如下
执行效果如下
后续计划
参考
https://github.com/dev-assistant/skykoma-plugin-idea
https://github.com/datalayer/jupyter-mcp-server/tree/main/jupyter_mcp_server
0 Comments