clikernel 0.2.10__tar.gz → 0.2.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clikernel-0.2.10 → clikernel-0.2.11}/CHANGELOG.md +7 -0
- {clikernel-0.2.10/clikernel.egg-info → clikernel-0.2.11}/PKG-INFO +2 -2
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/__init__.py +2 -2
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/cli.py +5 -3
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/core.py +22 -15
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/mcp.py +17 -10
- {clikernel-0.2.10 → clikernel-0.2.11/clikernel.egg-info}/PKG-INFO +2 -2
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel.egg-info/requires.txt +1 -1
- {clikernel-0.2.10 → clikernel-0.2.11}/pyproject.toml +1 -1
- {clikernel-0.2.10 → clikernel-0.2.11}/LICENSE +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/MANIFEST.in +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/README.md +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/_modidx.py +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/skill.py +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel/stream.py +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel.egg-info/SOURCES.txt +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel.egg-info/dependency_links.txt +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel.egg-info/entry_points.txt +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/clikernel.egg-info/top_level.txt +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/setup.cfg +0 -0
- {clikernel-0.2.10 → clikernel-0.2.11}/tests/test_stream.py +0 -0
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- do not remove -->
|
|
4
4
|
|
|
5
|
+
## 0.2.11
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
|
|
9
|
+
- Rewrite module and API docs for clarity, harden session cleanup with try/finally and grouped errors, negotiate MCP protocol version ([#49](https://github.com/AnswerDotAI/clikernel/issues/49))
|
|
10
|
+
|
|
11
|
+
|
|
5
12
|
## 0.2.10
|
|
6
13
|
|
|
7
14
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
4
4
|
Summary: Serve persistent Jupyter kernels to LLMs as concise text, over MCP or a plain stream protocol
|
|
5
5
|
Author: clikernel contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,7 +14,7 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: fastcore>=2.2.2
|
|
15
15
|
Requires-Dist: jupyasyncclient>=0.2.10
|
|
16
16
|
Requires-Dist: jupywire>=0.1.9
|
|
17
|
-
Requires-Dist: mcpmini>=0.0.
|
|
17
|
+
Requires-Dist: mcpmini>=0.0.5
|
|
18
18
|
Requires-Dist: rustygate>=0.1.11
|
|
19
19
|
Requires-Dist: httpx
|
|
20
20
|
Provides-Extra: dev
|
|
@@ -4,8 +4,8 @@ Modules:
|
|
|
4
4
|
|
|
5
5
|
- `clikernel.cli`: The stream-protocol frontend: the service on stdin/stdout for token-reading clients
|
|
6
6
|
- `clikernel.core`: Gateway naming, startup delivery, and MCP sessions on rustygate gateways
|
|
7
|
-
- `clikernel.mcp`:
|
|
7
|
+
- `clikernel.mcp`: Route stdio MCP requests to rustygate gateways
|
|
8
8
|
- `clikernel.skill`: Use the `clikernel` MCP session as the default workspace for Python work: reading and changing files, notebook work, trying things out, checking how a library behaves, and reshaping data. One session stays open, so imports and variables carry between calls. Read this before writing, running, or debugging Python code in a session with `clikernel` connected.
|
|
9
9
|
- `clikernel.stream`: Streaming JSON-lines worker protocol: nbformat-shaped output events, and a supervisor for select-based UIs."""
|
|
10
10
|
|
|
11
|
-
__version__ = "0.2.
|
|
11
|
+
__version__ = "0.2.11"
|
|
@@ -147,7 +147,9 @@ def main(
|
|
|
147
147
|
except KeyboardInterrupt: asyncio.run_coroutine_threadsafe(g.call('interrupt'), loop)
|
|
148
148
|
try: serve_stream(execute, info=info, should_exit=lambda: stop)
|
|
149
149
|
finally:
|
|
150
|
-
run(g.aclose())
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
try: run(g.aclose())
|
|
151
|
+
finally:
|
|
152
|
+
try:
|
|
153
|
+
if child: child.stop()
|
|
154
|
+
finally: loop.call_soon_threadsafe(loop.stop)
|
|
153
155
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"""Gateway naming, startup delivery, and MCP sessions on rustygate gateways
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Rustygate](https://github.com/AnswerDotAI/rustygate) hosts kernels and serves MCP requests at `POST /mcp`. Clikernel runs alongside it for the duration of a conversation. It routes the LLM client's stdio MCP requests to gateways.
|
|
4
|
+
|
|
5
|
+
This module connects to those gateways. `gateways.toml` gives them names. `session_defaults` supplies kernel startup code and, for a local gateway, the conversation's working directory and environment. It combines `startup.py` and `inspectors.py` into source that can run on another machine.
|
|
6
|
+
|
|
7
|
+
`Gateway` represents one MCP session on one gateway. `default_gateway` connects to the local gateway or starts a child process if it cannot connect. The caller must stop any returned child. The MCP router does this when the conversation ends.
|
|
8
|
+
|
|
9
|
+
Rustygate tracks the session's current kernel and which kernels it created. Ending a session stops its autoclose kernels. It leaves other kernels running. Stopping an owned gateway process stops all kernels in that process.
|
|
4
10
|
|
|
5
11
|
Docs: https://AnswerDotAI.github.io/clikernel/core.html.md"""
|
|
6
12
|
|
|
@@ -21,16 +27,16 @@ from . import __version__
|
|
|
21
27
|
DEFAULT_URL = 'http://127.0.0.1:8787'
|
|
22
28
|
|
|
23
29
|
def cfg_dir():
|
|
24
|
-
"
|
|
30
|
+
"Return the clikernel configuration directory."
|
|
25
31
|
return xdg_config_home()/'clikernel'
|
|
26
32
|
|
|
27
33
|
def gateways(cfgdir=None):
|
|
28
|
-
"
|
|
34
|
+
"Read named gateways as `{name: {url, token | token_env, verify}}` from `gateways.toml`."
|
|
29
35
|
p = (Path(cfgdir) if cfgdir else cfg_dir())/'gateways.toml'
|
|
30
36
|
return tomllib.loads(p.read_text()).get('gateways', {}) if p.exists() else {}
|
|
31
37
|
|
|
32
38
|
def resolve(host='', cfgdir=None):
|
|
33
|
-
"
|
|
39
|
+
"Resolve an empty host, URL, or configured gateway name to `(url, token, verify)`."
|
|
34
40
|
if not host: return os.environ.get('CLIKERNEL_HOST', DEFAULT_URL), os.environ.get('CLIKERNEL_TOKEN'), True
|
|
35
41
|
if '://' in host: return host, os.environ.get('CLIKERNEL_TOKEN'), True
|
|
36
42
|
cfgdir = Path(cfgdir) if cfgdir else cfg_dir()
|
|
@@ -41,7 +47,7 @@ def resolve(host='', cfgdir=None):
|
|
|
41
47
|
|
|
42
48
|
# %% ../nbs/00_core.ipynb #0d846754
|
|
43
49
|
def _startup_src(src, path):
|
|
44
|
-
"
|
|
50
|
+
"Wrap startup source with `__file__` set to its path during execution and deleted afterwards."
|
|
45
51
|
return f'''__file__ = {str(path)!r}
|
|
46
52
|
try: exec(compile({src!r}, __file__, 'exec'))
|
|
47
53
|
finally: del __file__'''
|
|
@@ -93,7 +99,7 @@ def _inspector_setup(src):
|
|
|
93
99
|
|
|
94
100
|
# %% ../nbs/00_core.ipynb #1362e6ce
|
|
95
101
|
def startup_src(cfgdir=None):
|
|
96
|
-
"
|
|
102
|
+
"Combine wrapped `startup.py` source with the inspector installer, in that order."
|
|
97
103
|
d = Path(cfgdir) if cfgdir else cfg_dir()
|
|
98
104
|
parts = []
|
|
99
105
|
if (p := d/'startup.py').exists(): parts.append(_startup_src(p.read_text(), p))
|
|
@@ -101,7 +107,7 @@ def startup_src(cfgdir=None):
|
|
|
101
107
|
return '\n'.join(parts)
|
|
102
108
|
|
|
103
109
|
def session_defaults(cfgdir=None, quiet=False, local=True):
|
|
104
|
-
"
|
|
110
|
+
"Return startup and quiet settings for rustygate initialization. Local sessions also include cwd and env."
|
|
105
111
|
d = dict(startup=startup_src(cfgdir), quiet=quiet)
|
|
106
112
|
if local:
|
|
107
113
|
d['cwd'] = os.getcwd()
|
|
@@ -110,7 +116,7 @@ def session_defaults(cfgdir=None, quiet=False, local=True):
|
|
|
110
116
|
|
|
111
117
|
# %% ../nbs/00_core.ipynb #10ed59fc
|
|
112
118
|
class Gateway:
|
|
113
|
-
"
|
|
119
|
+
"Open an MCP session on rustygate, call its tools, and end the session with DELETE."
|
|
114
120
|
def __init__(self,
|
|
115
121
|
url, # The gateway base URL, e.g. 'http://127.0.0.1:8787'
|
|
116
122
|
token=None, # Gateway auth token, sent as a bearer token
|
|
@@ -121,17 +127,18 @@ class Gateway:
|
|
|
121
127
|
self.tr = HTTPTransport(f"{url.rstrip('/')}/mcp", token=token, http_client=client)
|
|
122
128
|
|
|
123
129
|
async def rpc(self, method, **params):
|
|
124
|
-
"
|
|
130
|
+
"Send a JSON-RPC request and return its result. Raise `RuntimeError` for protocol errors."
|
|
125
131
|
self._id += 1
|
|
126
132
|
r = await self.tr.send(jreq(method, self._id, **params))
|
|
127
133
|
if 'error' in r: raise RuntimeError(f"{r['error']['code']}: {r['error']['message']}")
|
|
128
134
|
return r['result']
|
|
129
135
|
|
|
130
136
|
async def initialize(self, defaults=None):
|
|
131
|
-
"Open the MCP session
|
|
137
|
+
"Open the MCP session with `defaults` in the `rustygate` extension. Return self."
|
|
132
138
|
await self.tr.start()
|
|
133
139
|
self.info = await self.rpc('initialize', protocolVersion='2025-11-25', capabilities={},
|
|
134
140
|
clientInfo=dict(name='clikernel', version=__version__), rustygate=defaults or {})
|
|
141
|
+
self.tr.proto = self.info['protocolVersion']
|
|
135
142
|
await self.tr.send(jreq('notifications/initialized'))
|
|
136
143
|
return self
|
|
137
144
|
|
|
@@ -139,23 +146,23 @@ class Gateway:
|
|
|
139
146
|
async def call(self, name, **args): return await self.rpc('tools/call', name=name, arguments=args)
|
|
140
147
|
|
|
141
148
|
async def text(self, name, **args):
|
|
142
|
-
"
|
|
149
|
+
"Join a tool reply's text blocks. Raise `RuntimeError` for `isError` replies."
|
|
143
150
|
r = await self.call(name, **args)
|
|
144
151
|
t = ''.join(c.get('text','') for c in r['content'] if c['type'] == 'text')
|
|
145
152
|
if r.get('isError'): raise RuntimeError(t)
|
|
146
153
|
return t
|
|
147
154
|
|
|
148
155
|
async def aclose(self):
|
|
149
|
-
"
|
|
150
|
-
await self.tr.delete()
|
|
151
|
-
await self.tr.aclose()
|
|
156
|
+
"Request session termination and close the HTTP connection."
|
|
157
|
+
try: await self.tr.delete()
|
|
158
|
+
finally: await self.tr.aclose()
|
|
152
159
|
|
|
153
160
|
# %% ../nbs/00_core.ipynb #0d054375
|
|
154
161
|
async def default_gateway(
|
|
155
162
|
cfgdir=None, # Config dir for `session_defaults` (the standard one if None)
|
|
156
163
|
quiet=False, # Keep startup output out of replies?
|
|
157
164
|
):
|
|
158
|
-
"
|
|
165
|
+
"Return an initialized `Gateway` and its new child process, or None if it reused a gateway. The caller must stop any child."
|
|
159
166
|
url, token, verify = resolve('', cfgdir)
|
|
160
167
|
d = session_defaults(cfgdir, quiet)
|
|
161
168
|
try: return await Gateway(url, token, verify).initialize(d), None
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Route stdio MCP requests to rustygate gateways
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Claude Code starts `clikernel-mcp` for each conversation. Its `Router` accepts MCP messages over stdio and sends tool calls to rustygate gateways. Rustygate implements the tools. The router gets their schemas from the local gateway and adds `host` to `list_kernels`, `use_kernel`, and `create`.
|
|
4
|
+
|
|
5
|
+
Each host has a separate gateway session. The router remembers one current host for calls without an explicit `host`. It removes `host` before forwarding a call but preserves the JSON-RPC request id. Cancellation notifications also retain their original ids.
|
|
6
|
+
|
|
7
|
+
`main` runs the router and closes its gateway sessions on exit. Closing a session sends an HTTP DELETE. The gateway then stops the kernels that session created with autoclose. If the router started a child gateway, it stops that process too.
|
|
4
8
|
|
|
5
9
|
Docs: https://AnswerDotAI.github.io/clikernel/mcp.html.md"""
|
|
6
10
|
|
|
@@ -23,7 +27,7 @@ HOST_PARAM = {'type': 'string', 'description': 'Gateway to target: a gateways.to
|
|
|
23
27
|
HOSTED = ('list_kernels', 'use_kernel', 'create')
|
|
24
28
|
|
|
25
29
|
class Router:
|
|
26
|
-
"
|
|
30
|
+
"Route stdio MCP to rustygate with one session per host and one current host."
|
|
27
31
|
def __init__(self,
|
|
28
32
|
cfgdir=None, # Config dir for `session_defaults` and `gateways.toml` (the standard one if None)
|
|
29
33
|
quiet=False, # Keep startup output out of replies?
|
|
@@ -31,7 +35,7 @@ class Router:
|
|
|
31
35
|
self.cfgdir,self.quiet,self.sessions,self.cur,self.child = cfgdir,quiet,{},'',None
|
|
32
36
|
|
|
33
37
|
async def session(self, host=''):
|
|
34
|
-
"
|
|
38
|
+
"Return the session for `host`, initializing it on first use. Empty `host` selects the default local gateway."
|
|
35
39
|
if host not in self.sessions:
|
|
36
40
|
if host:
|
|
37
41
|
url, token, verify = resolve(host, self.cfgdir)
|
|
@@ -40,15 +44,18 @@ class Router:
|
|
|
40
44
|
return self.sessions[host]
|
|
41
45
|
|
|
42
46
|
async def aclose(self):
|
|
43
|
-
"
|
|
44
|
-
for s in self.sessions.values()
|
|
45
|
-
|
|
47
|
+
"Close all sessions and their autoclose kernels, then stop any owned gateway."
|
|
48
|
+
try: results = await asyncio.gather(*(s.aclose() for s in self.sessions.values()), return_exceptions=True)
|
|
49
|
+
finally:
|
|
50
|
+
if self.child: self.child.stop()
|
|
51
|
+
errors = [r for r in results if isinstance(r, BaseException)]
|
|
52
|
+
if errors: raise BaseExceptionGroup('MCP session cleanup failed', errors)
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
# %% ../nbs/01_mcp.ipynb #79da6a3e
|
|
49
56
|
@patch
|
|
50
57
|
async def tools(self:Router):
|
|
51
|
-
"
|
|
58
|
+
"Return the local gateway's tools with `host` added to `list_kernels`, `use_kernel`, and `create`."
|
|
52
59
|
ts = await (await self.session()).tools()
|
|
53
60
|
for t in ts:
|
|
54
61
|
if t['name'] in HOSTED: t['inputSchema'].setdefault('properties', {})['host'] = dict(HOST_PARAM)
|
|
@@ -56,7 +63,7 @@ async def tools(self:Router):
|
|
|
56
63
|
|
|
57
64
|
@patch
|
|
58
65
|
async def dispatch(self:Router, msg, requester=None):
|
|
59
|
-
"
|
|
66
|
+
"Answer initialization and ping locally. Forward tool calls to gateways."
|
|
60
67
|
method,id = msg.get('method'), msg.get('id')
|
|
61
68
|
try:
|
|
62
69
|
if method == 'initialize':
|
|
@@ -83,7 +90,7 @@ async def dispatch(self:Router, msg, requester=None):
|
|
|
83
90
|
def main(
|
|
84
91
|
quiet:store_true=False, # Keep startup output out of replies
|
|
85
92
|
):
|
|
86
|
-
"
|
|
93
|
+
"Run `clikernel-mcp` as a stdio server."
|
|
87
94
|
async def _main():
|
|
88
95
|
router = Router(quiet=quiet)
|
|
89
96
|
try: await serve_stdio(router)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
4
4
|
Summary: Serve persistent Jupyter kernels to LLMs as concise text, over MCP or a plain stream protocol
|
|
5
5
|
Author: clikernel contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,7 +14,7 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: fastcore>=2.2.2
|
|
15
15
|
Requires-Dist: jupyasyncclient>=0.2.10
|
|
16
16
|
Requires-Dist: jupywire>=0.1.9
|
|
17
|
-
Requires-Dist: mcpmini>=0.0.
|
|
17
|
+
Requires-Dist: mcpmini>=0.0.5
|
|
18
18
|
Requires-Dist: rustygate>=0.1.11
|
|
19
19
|
Requires-Dist: httpx
|
|
20
20
|
Provides-Extra: dev
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|