scmcp-shared 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
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.
- scmcp_shared/__init__.py +1 -3
- scmcp_shared/agent.py +38 -21
- scmcp_shared/backend.py +44 -0
- scmcp_shared/cli.py +75 -46
- scmcp_shared/kb.py +139 -0
- scmcp_shared/logging_config.py +6 -8
- scmcp_shared/mcp_base.py +184 -0
- scmcp_shared/schema/io.py +101 -59
- scmcp_shared/schema/pl.py +386 -490
- scmcp_shared/schema/pp.py +514 -265
- scmcp_shared/schema/preset/__init__.py +15 -0
- scmcp_shared/schema/preset/io.py +103 -0
- scmcp_shared/schema/preset/pl.py +843 -0
- scmcp_shared/schema/preset/pp.py +616 -0
- scmcp_shared/schema/preset/tl.py +917 -0
- scmcp_shared/schema/preset/util.py +123 -0
- scmcp_shared/schema/tl.py +355 -407
- scmcp_shared/schema/util.py +57 -72
- scmcp_shared/server/__init__.py +5 -10
- scmcp_shared/server/auto.py +15 -11
- scmcp_shared/server/code.py +3 -0
- scmcp_shared/server/preset/__init__.py +14 -0
- scmcp_shared/server/{io.py → preset/io.py} +26 -22
- scmcp_shared/server/{pl.py → preset/pl.py} +162 -78
- scmcp_shared/server/{pp.py → preset/pp.py} +123 -65
- scmcp_shared/server/{tl.py → preset/tl.py} +142 -79
- scmcp_shared/server/{util.py → preset/util.py} +123 -66
- scmcp_shared/server/rag.py +13 -0
- scmcp_shared/util.py +109 -38
- {scmcp_shared-0.4.0.dist-info → scmcp_shared-0.6.0.dist-info}/METADATA +6 -2
- scmcp_shared-0.6.0.dist-info/RECORD +35 -0
- scmcp_shared/server/base.py +0 -148
- scmcp_shared-0.4.0.dist-info/RECORD +0 -24
- {scmcp_shared-0.4.0.dist-info → scmcp_shared-0.6.0.dist-info}/WHEEL +0 -0
- {scmcp_shared-0.4.0.dist-info → scmcp_shared-0.6.0.dist-info}/licenses/LICENSE +0 -0
scmcp_shared/util.py
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
import inspect
|
2
2
|
import os
|
3
|
-
from enum import Enum
|
4
3
|
from pathlib import Path
|
5
4
|
from fastmcp.server.dependencies import get_context
|
6
5
|
from fastmcp.exceptions import ToolError
|
7
6
|
import asyncio
|
8
7
|
import nest_asyncio
|
8
|
+
from pydantic import BaseModel
|
9
9
|
|
10
10
|
|
11
11
|
def get_env(key):
|
12
12
|
return os.environ.get(f"SCMCP_{key.upper()}")
|
13
13
|
|
14
|
-
|
14
|
+
|
15
15
|
def filter_args(request, func, **extra_kwargs):
|
16
16
|
kwargs = request.model_dump()
|
17
17
|
args = request.model_fields_set
|
@@ -25,7 +25,7 @@ def filter_args(request, func, **extra_kwargs):
|
|
25
25
|
def add_op_log(adata, func, kwargs, adinfo):
|
26
26
|
import hashlib
|
27
27
|
import json
|
28
|
-
|
28
|
+
|
29
29
|
if "operation" not in adata.uns:
|
30
30
|
adata.uns["operation"] = {}
|
31
31
|
adata.uns["operation"]["op"] = {}
|
@@ -41,26 +41,27 @@ def add_op_log(adata, func, kwargs, adinfo):
|
|
41
41
|
else:
|
42
42
|
func_name = str(func)
|
43
43
|
new_kwargs = {**adinfo.model_dump()}
|
44
|
-
for k,v in kwargs.items():
|
44
|
+
for k, v in kwargs.items():
|
45
45
|
if isinstance(v, tuple):
|
46
46
|
new_kwargs[k] = list(v)
|
47
47
|
else:
|
48
48
|
new_kwargs[k] = v
|
49
49
|
try:
|
50
50
|
kwargs_str = json.dumps(new_kwargs, sort_keys=True)
|
51
|
-
except:
|
52
|
-
|
51
|
+
except Exception as e:
|
52
|
+
print(e)
|
53
|
+
kwargs_str = f"{e}" + str(new_kwargs)
|
53
54
|
hash_input = f"{func_name}:{kwargs_str}"
|
54
55
|
hash_key = hashlib.md5(hash_input.encode()).hexdigest()
|
55
56
|
adata.uns["operation"]["op"][hash_key] = {func_name: new_kwargs}
|
56
57
|
adata.uns["operation"]["opid"] = list(adata.uns["operation"]["opid"])
|
57
58
|
adata.uns["operation"]["opid"].append(hash_key)
|
58
59
|
from .logging_config import setup_logger
|
60
|
+
|
59
61
|
logger = setup_logger(log_file=get_env("LOG_FILE"))
|
60
62
|
logger.info(f"{func}: {new_kwargs}")
|
61
63
|
|
62
64
|
|
63
|
-
|
64
65
|
def save_fig_path(axes, file):
|
65
66
|
from matplotlib.axes import Axes
|
66
67
|
|
@@ -76,12 +77,14 @@ def save_fig_path(axes, file):
|
|
76
77
|
ax.figure.savefig(file_path)
|
77
78
|
elif isinstance(axes, Axes):
|
78
79
|
axes.figure.savefig(file_path)
|
79
|
-
elif hasattr(axes,
|
80
|
+
elif hasattr(axes, "savefig"): # if Figure
|
80
81
|
axes.savefig(file_path)
|
81
|
-
elif hasattr(axes,
|
82
|
+
elif hasattr(axes, "save"): # for plotnine.ggplot.ggplot
|
82
83
|
axes.save(file_path)
|
83
84
|
else:
|
84
|
-
raise ValueError(
|
85
|
+
raise ValueError(
|
86
|
+
f"axes must be a Axes or plotnine object, but got {type(axes)}"
|
87
|
+
)
|
85
88
|
return file_path
|
86
89
|
except Exception as e:
|
87
90
|
raise e
|
@@ -102,7 +105,7 @@ def savefig(axes, func=None, **kwargs):
|
|
102
105
|
kwargs.pop("save", None)
|
103
106
|
kwargs.pop("show", None)
|
104
107
|
args = []
|
105
|
-
for k,v in kwargs.items():
|
108
|
+
for k, v in kwargs.items():
|
106
109
|
if isinstance(v, (tuple, list, set)):
|
107
110
|
v = v[:3] ## show first 3 elements
|
108
111
|
args.append(f"{k}-{'-'.join([str(i) for i in v])}")
|
@@ -119,7 +122,7 @@ def savefig(axes, func=None, **kwargs):
|
|
119
122
|
raise PermissionError("You don't have permission to save figure")
|
120
123
|
except Exception as e:
|
121
124
|
raise e
|
122
|
-
transport = get_env("TRANSPORT")
|
125
|
+
transport = get_env("TRANSPORT")
|
123
126
|
if transport == "stdio":
|
124
127
|
return fig_path
|
125
128
|
else:
|
@@ -129,36 +132,41 @@ def savefig(axes, func=None, **kwargs):
|
|
129
132
|
return fig_path
|
130
133
|
|
131
134
|
|
132
|
-
|
133
135
|
async def get_figure(request):
|
134
136
|
from starlette.responses import FileResponse, Response
|
135
137
|
|
136
138
|
figure_name = request.path_params["figure_name"]
|
137
139
|
figure_path = f"./figures/{figure_name}"
|
138
|
-
|
140
|
+
|
139
141
|
if not os.path.isfile(figure_path):
|
140
|
-
return Response(
|
141
|
-
|
142
|
+
return Response(
|
143
|
+
content={"error": "figure not found"}, media_type="application/json"
|
144
|
+
)
|
145
|
+
|
142
146
|
return FileResponse(figure_path)
|
143
147
|
|
144
148
|
|
145
149
|
def add_figure_route(server):
|
146
150
|
from starlette.routing import Route
|
147
|
-
|
151
|
+
|
152
|
+
server._additional_http_routes = [
|
153
|
+
Route("/figures/{figure_name}", endpoint=get_figure)
|
154
|
+
]
|
148
155
|
|
149
156
|
|
150
157
|
async def async_forward_request(func, request, adinfo, **kwargs):
|
151
158
|
from fastmcp import Client
|
159
|
+
|
152
160
|
forward_url = get_env("FORWARD")
|
153
161
|
request_kwargs = request.model_dump()
|
154
162
|
request_args = request.model_fields_set
|
155
163
|
func_kwargs = {
|
156
164
|
"request": {k: request_kwargs.get(k) for k in request_args},
|
157
|
-
"adinfo": adinfo.model_dump()
|
165
|
+
"adinfo": adinfo.model_dump(),
|
158
166
|
}
|
159
167
|
if not forward_url:
|
160
168
|
return None
|
161
|
-
|
169
|
+
|
162
170
|
client = Client(forward_url)
|
163
171
|
async with client:
|
164
172
|
tools = await client.list_tools()
|
@@ -169,13 +177,12 @@ async def async_forward_request(func, request, adinfo, **kwargs):
|
|
169
177
|
except ToolError as e:
|
170
178
|
raise ToolError(e)
|
171
179
|
except Exception as e:
|
172
|
-
if hasattr(e,
|
180
|
+
if hasattr(e, "__context__") and e.__context__:
|
173
181
|
raise Exception(f"{str(e.__context__)}")
|
174
182
|
else:
|
175
183
|
raise e
|
176
184
|
|
177
185
|
|
178
|
-
|
179
186
|
def forward_request(func, request, adinfo, **kwargs):
|
180
187
|
"""Synchronous wrapper for forward_request"""
|
181
188
|
try:
|
@@ -186,12 +193,13 @@ def forward_request(func, request, adinfo, **kwargs):
|
|
186
193
|
# If we're in a running event loop, use create_task
|
187
194
|
async def _run():
|
188
195
|
return await async_forward_request(func, request, adinfo, **kwargs)
|
196
|
+
|
189
197
|
return loop.run_until_complete(_run())
|
190
198
|
else:
|
191
199
|
# If no event loop is running, use asyncio.run()
|
192
200
|
return asyncio.run(async_forward_request(func, request, adinfo, **kwargs))
|
193
201
|
except Exception as e:
|
194
|
-
if hasattr(e,
|
202
|
+
if hasattr(e, "__context__") and e.__context__:
|
195
203
|
raise Exception(f"{str(e.__context__)}")
|
196
204
|
else:
|
197
205
|
raise e
|
@@ -203,7 +211,9 @@ def obsm2adata(adata, obsm_key):
|
|
203
211
|
if obsm_key not in adata.obsm_keys():
|
204
212
|
raise ValueError(f"key {obsm_key} not found in adata.obsm")
|
205
213
|
else:
|
206
|
-
return AnnData(
|
214
|
+
return AnnData(
|
215
|
+
adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm, uns=adata.uns
|
216
|
+
)
|
207
217
|
|
208
218
|
|
209
219
|
def get_ads():
|
@@ -213,7 +223,11 @@ def get_ads():
|
|
213
223
|
|
214
224
|
|
215
225
|
def generate_msg(adinfo, adata, ads):
|
216
|
-
return {
|
226
|
+
return {
|
227
|
+
"sampleid": adinfo.sampleid or ads.active_id,
|
228
|
+
"adtype": adinfo.adtype,
|
229
|
+
"adata": adata,
|
230
|
+
}
|
217
231
|
|
218
232
|
|
219
233
|
def sc_like_plot(plot_func, adata, request, adinfo, **kwargs):
|
@@ -231,7 +245,9 @@ def sc_like_plot(plot_func, adata, request, adinfo, **kwargs):
|
|
231
245
|
def filter_tools(mcp, include_tools=None, exclude_tools=None):
|
232
246
|
import asyncio
|
233
247
|
import copy
|
248
|
+
|
234
249
|
mcp = copy.deepcopy(mcp)
|
250
|
+
|
235
251
|
async def _filter_tools(mcp, include_tools=None, exclude_tools=None):
|
236
252
|
tools = await mcp.get_tools()
|
237
253
|
for tool in tools:
|
@@ -240,43 +256,98 @@ def filter_tools(mcp, include_tools=None, exclude_tools=None):
|
|
240
256
|
if include_tools and tool not in include_tools:
|
241
257
|
mcp.remove_tool(tool)
|
242
258
|
return mcp
|
259
|
+
|
243
260
|
return asyncio.run(_filter_tools(mcp, include_tools, exclude_tools))
|
244
261
|
|
245
262
|
|
246
|
-
|
247
263
|
def set_env(log_file, forward, transport, host, port):
|
248
264
|
if log_file is not None:
|
249
|
-
os.environ[
|
265
|
+
os.environ["SCMCP_LOG_FILE"] = log_file
|
250
266
|
if forward is not None:
|
251
|
-
os.environ[
|
252
|
-
os.environ[
|
253
|
-
os.environ[
|
254
|
-
os.environ[
|
255
|
-
|
267
|
+
os.environ["SCMCP_FORWARD"] = forward
|
268
|
+
os.environ["SCMCP_TRANSPORT"] = transport
|
269
|
+
os.environ["SCMCP_HOST"] = host
|
270
|
+
os.environ["SCMCP_PORT"] = str(port)
|
256
271
|
|
257
272
|
|
258
273
|
def setup_mcp(mcp, sub_mcp_dic, modules=None):
|
259
274
|
import asyncio
|
275
|
+
|
260
276
|
if modules is None or modules == "all":
|
261
277
|
modules = sub_mcp_dic.keys()
|
262
278
|
for module in modules:
|
263
279
|
asyncio.run(mcp.import_server(module, sub_mcp_dic[module]))
|
264
280
|
return mcp
|
265
281
|
|
266
|
-
|
282
|
+
|
283
|
+
def _update_args(mcp, func, args_dic: dict):
|
267
284
|
for args, property_dic in args_dic.items():
|
268
285
|
for pk, v in property_dic.items():
|
269
|
-
mcp._tool_manager._tools[func].parameters["properties"][
|
270
|
-
|
286
|
+
mcp._tool_manager._tools[func].parameters["properties"][
|
287
|
+
"request"
|
288
|
+
].setdefault(pk, {})
|
289
|
+
mcp._tool_manager._tools[func].parameters["properties"]["request"][pk][
|
290
|
+
args
|
291
|
+
] = v
|
271
292
|
|
272
293
|
|
273
|
-
def update_mcp_args(mcp, tool_args
|
274
|
-
tools = mcp._tool_manager._tools.keys()
|
275
|
-
for tool in tool_args:
|
294
|
+
def update_mcp_args(mcp, tool_args: dict):
|
295
|
+
# tools = mcp._tool_manager._tools.keys()
|
296
|
+
for tool in tool_args:
|
276
297
|
_update_args(mcp, tool, tool_args[tool])
|
277
298
|
|
278
299
|
|
279
300
|
def check_adata(adata, adinfo, ads):
|
280
301
|
sampleid = adinfo.sampleid or ads.active_id
|
281
302
|
if sampleid != adata.uns["scmcp_sampleid"]:
|
282
|
-
raise ValueError(
|
303
|
+
raise ValueError(
|
304
|
+
f"sampleid mismatch: {sampleid} != {adata.uns['scmcp_sampleid']}"
|
305
|
+
)
|
306
|
+
|
307
|
+
|
308
|
+
def get_nbm():
|
309
|
+
ctx = get_context()
|
310
|
+
nbm = ctx.request_context.lifespan_context
|
311
|
+
return nbm
|
312
|
+
|
313
|
+
|
314
|
+
def parse_args(
|
315
|
+
kwargs: BaseModel | dict,
|
316
|
+
positional_args: list[str] | str | None = None,
|
317
|
+
func_args: list[str] | str | None = None,
|
318
|
+
) -> str:
|
319
|
+
if isinstance(kwargs, BaseModel):
|
320
|
+
kwargs = kwargs.model_dump()
|
321
|
+
elif isinstance(kwargs, dict):
|
322
|
+
kwargs = kwargs
|
323
|
+
else:
|
324
|
+
raise ValueError(f"Invalid type: {type(kwargs)}")
|
325
|
+
|
326
|
+
if func_args is not None:
|
327
|
+
if isinstance(func_args, str):
|
328
|
+
func_args = [func_args]
|
329
|
+
else:
|
330
|
+
func_args = []
|
331
|
+
kwargs_str_ls = []
|
332
|
+
if positional_args is not None:
|
333
|
+
if isinstance(positional_args, str):
|
334
|
+
kwargs_str_ls.append(kwargs.pop(positional_args))
|
335
|
+
elif isinstance(positional_args, list):
|
336
|
+
for arg in positional_args:
|
337
|
+
kwargs_str_ls.append(kwargs.pop(arg))
|
338
|
+
|
339
|
+
extra_kwargs = kwargs.pop("kwargs", {})
|
340
|
+
for k, v in kwargs.items():
|
341
|
+
if k in func_args:
|
342
|
+
kwargs_str_ls.append(f"{k}={v}")
|
343
|
+
continue
|
344
|
+
if isinstance(v, (list, tuple, dict, int, float, bool)):
|
345
|
+
kwargs_str_ls.append(f"{k}={v}")
|
346
|
+
elif isinstance(v, str):
|
347
|
+
kwargs_str_ls.append(f"{k}='{v}'")
|
348
|
+
|
349
|
+
if extra_kwargs:
|
350
|
+
kwargs_str_ls.append(f"**{extra_kwargs}")
|
351
|
+
|
352
|
+
kwargs_str = ", ".join(kwargs_str_ls)
|
353
|
+
return kwargs_str
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: scmcp_shared
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: A shared function libray for scmcphub
|
5
5
|
Project-URL: Homepage, http://scmcphub.org/
|
6
6
|
Project-URL: Repository, https://github.com/scmcphub/scmcp-shared
|
@@ -37,12 +37,16 @@ License: BSD 3-Clause License
|
|
37
37
|
License-File: LICENSE
|
38
38
|
Keywords: AI,agent,bioinformatics,llm,mcp,model context protocol,scRNA-seq,single cell
|
39
39
|
Requires-Python: >=3.10
|
40
|
+
Requires-Dist: abcoder
|
41
|
+
Requires-Dist: agno
|
40
42
|
Requires-Dist: fastmcp>=2.7.0
|
41
43
|
Requires-Dist: igraph
|
42
|
-
Requires-Dist:
|
44
|
+
Requires-Dist: lancedb
|
43
45
|
Requires-Dist: leidenalg
|
44
46
|
Requires-Dist: mcp>=1.8.0
|
45
47
|
Requires-Dist: nest-asyncio
|
48
|
+
Requires-Dist: openai
|
49
|
+
Requires-Dist: requests
|
46
50
|
Requires-Dist: scanpy
|
47
51
|
Description-Content-Type: text/markdown
|
48
52
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
scmcp_shared/__init__.py,sha256=cID1jLnC_vj48GgMN6Yb1FA3JsQ95zNmCHmRYE8TFhY,22
|
2
|
+
scmcp_shared/agent.py,sha256=pC1g09BgUrahxoYN1JlxPi1_kOeuwWAGwMtR4oKUruc,1142
|
3
|
+
scmcp_shared/backend.py,sha256=PyxHhg7ThHE5u6XtWwleAiJ3aqdLHbXoXuyw5QeF4Qw,1695
|
4
|
+
scmcp_shared/cli.py,sha256=zYz_E3SlZ_mtYOrSdDJhDmHsppkwzEi2rZ6D3a_ga1c,5603
|
5
|
+
scmcp_shared/kb.py,sha256=Wy0ib43DdvIXqSUStRQzK-It-fGqZI1lb4mQY6UnJ3o,4491
|
6
|
+
scmcp_shared/logging_config.py,sha256=Z5jLgOCfir4UGxM-ZQvYv7Iyl_zF78qqPNQ_aSE77f0,838
|
7
|
+
scmcp_shared/mcp_base.py,sha256=v8sBqObTVzLEvoeQe3la2cfnDWZE7DkY07SqEb_1srA,7277
|
8
|
+
scmcp_shared/util.py,sha256=LNrpoHBdA4x-Tt16uT_9A72_QZ41SrqZ0zy3Ozwa858,11163
|
9
|
+
scmcp_shared/schema/__init__.py,sha256=Kwkc7kPLjExOlxt1sWEy_5qa96MvOS8sNCMlZa6yRg8,737
|
10
|
+
scmcp_shared/schema/io.py,sha256=wZxGDJzzeSbI5Nl5IIE9GJDkBynqYFakK21tnECvBNk,6090
|
11
|
+
scmcp_shared/schema/pl.py,sha256=vheuv1itA-FkhAb9c7kkCpHWgbmJGhWJf8nAYkfzbBI,28228
|
12
|
+
scmcp_shared/schema/pp.py,sha256=PtbgMupJNBWmAx6lIoMy6RflByAasSnFB5PCytijS80,32300
|
13
|
+
scmcp_shared/schema/tl.py,sha256=YbeKoaUqJXcn2WIJn0Rzn5rq4qud_ncUdoaliQkZYwI,35358
|
14
|
+
scmcp_shared/schema/tool.py,sha256=U-VFrovAEr2FklS8cSJPimfhTSTHmskmLfCO7Ee0s2o,267
|
15
|
+
scmcp_shared/schema/util.py,sha256=1twQ9kiaKI6XubJIuYywG7vr-T2cCCblehhYXJQskls,4558
|
16
|
+
scmcp_shared/schema/preset/__init__.py,sha256=dw8bPW1LaQKtATc2wmkAkF-fHYGwIGxY3Aq9mHTL9HA,422
|
17
|
+
scmcp_shared/schema/preset/io.py,sha256=hP8LVbiZUaGU0mHCxHS3wmA3Q6OPEldvMACnD6gAn9Y,4558
|
18
|
+
scmcp_shared/schema/preset/pl.py,sha256=bURWdeJ1CVOyL78Zfof7aJIXiqtKUog5NuVxNm1mGNw,28152
|
19
|
+
scmcp_shared/schema/preset/pp.py,sha256=j6cnQlt2YuOIJwjl8x4JNojSVcXW1bVjnt6BZ8QHNzQ,21079
|
20
|
+
scmcp_shared/schema/preset/tl.py,sha256=cHne8cIJBFph5MENsgElx1MQUOi7LYc62M0OQppcZ40,33818
|
21
|
+
scmcp_shared/schema/preset/util.py,sha256=1twQ9kiaKI6XubJIuYywG7vr-T2cCCblehhYXJQskls,4558
|
22
|
+
scmcp_shared/server/__init__.py,sha256=qwJEKZnBjcTi7wXcPpeQ-EP4YY9jJo9BYXkfCyn662c,198
|
23
|
+
scmcp_shared/server/auto.py,sha256=FQKbUDuWlgfkcakLGm28lzrJTBm2vynwskHDdVLV2Dk,1966
|
24
|
+
scmcp_shared/server/code.py,sha256=_uNIZ7sVfpBAhX1qPJ8f3zquSEgZXA3jWkPaRx3ph9k,56
|
25
|
+
scmcp_shared/server/rag.py,sha256=tKENjR9PuzkektduaTYHf0hDDyo4ypAqCJTyulLSZn4,402
|
26
|
+
scmcp_shared/server/preset/__init__.py,sha256=2DdRR0MaipsxREZ4dWvnGfNRAaHQY7BGGVNgGQEyfbo,318
|
27
|
+
scmcp_shared/server/preset/io.py,sha256=2QigfUHR8HJtVdI4nIfRh0tm8WhVu8Hge1AAos6afZM,4059
|
28
|
+
scmcp_shared/server/preset/pl.py,sha256=pQx7yD1-vRSnNky_8tapI6vU1lqcZ5S4zs5y1aamtcc,16774
|
29
|
+
scmcp_shared/server/preset/pp.py,sha256=9WtTQ8aHnN_G_7Rm9hZsa7GU3A7xHBoNf8tE-gsDDHY,17114
|
30
|
+
scmcp_shared/server/preset/tl.py,sha256=LtrGMNkeTlTqe2Cyw1OwZXpeTbyGqgENYYC76BKXgpg,19716
|
31
|
+
scmcp_shared/server/preset/util.py,sha256=vek2SOEfiyAyCUEjwsFf1uqwT9YuzDHVQJ-dprIDWME,13519
|
32
|
+
scmcp_shared-0.6.0.dist-info/METADATA,sha256=j--ns9ClsoFmoTvxc-DeQD_N-aeE29cTzFX2A9LBROU,2514
|
33
|
+
scmcp_shared-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
34
|
+
scmcp_shared-0.6.0.dist-info/licenses/LICENSE,sha256=YNr1hpea195yq-wGtB8j-2dGtt7A5G00WENmxa7JGco,1495
|
35
|
+
scmcp_shared-0.6.0.dist-info/RECORD,,
|
scmcp_shared/server/base.py
DELETED
@@ -1,148 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
from fastmcp import FastMCP
|
3
|
-
from ..schema import AdataInfo
|
4
|
-
from ..util import filter_tools
|
5
|
-
from collections.abc import AsyncIterator
|
6
|
-
from contextlib import asynccontextmanager
|
7
|
-
import asyncio
|
8
|
-
from typing import Optional, List, Any, Iterable
|
9
|
-
from .auto import auto_mcp
|
10
|
-
|
11
|
-
class BaseMCP:
|
12
|
-
"""Base class for all Scanpy MCP classes."""
|
13
|
-
|
14
|
-
def __init__(self, name: str, include_tools: list = None, exclude_tools: list = None, AdataInfo = AdataInfo):
|
15
|
-
"""
|
16
|
-
Initialize BaseMCP with optional tool filtering.
|
17
|
-
|
18
|
-
Args:
|
19
|
-
name (str): Name of the MCP server
|
20
|
-
include_tools (list, optional): List of tool names to include. If None, all tools are included.
|
21
|
-
exclude_tools (list, optional): List of tool names to exclude. If None, no tools are excluded.
|
22
|
-
AdataInfo: The AdataInfo class to use for type annotations.
|
23
|
-
"""
|
24
|
-
self.mcp = FastMCP(name)
|
25
|
-
self.include_tools = include_tools
|
26
|
-
self.exclude_tools = exclude_tools
|
27
|
-
self.AdataInfo = AdataInfo
|
28
|
-
self._register_tools()
|
29
|
-
|
30
|
-
def _register_tools(self):
|
31
|
-
"""Register all tool methods with the FastMCP instance based on include/exclude filters"""
|
32
|
-
# Get all methods of the class
|
33
|
-
methods = inspect.getmembers(self, predicate=inspect.ismethod)
|
34
|
-
|
35
|
-
# Filter methods that start with _tool_
|
36
|
-
tool_methods = [tl_method() for name, tl_method in methods if name.startswith('_tool_')]
|
37
|
-
|
38
|
-
# Filter tools based on include/exclude lists
|
39
|
-
if self.include_tools is not None:
|
40
|
-
tool_methods = [tl for tl in tool_methods if tl.name in self.include_tools]
|
41
|
-
|
42
|
-
if self.exclude_tools is not None:
|
43
|
-
tool_methods = [tl for tl in tool_methods if tl.name not in self.exclude_tools]
|
44
|
-
|
45
|
-
# Register filtered tools
|
46
|
-
for tool in tool_methods:
|
47
|
-
# Get the function returned by the tool method
|
48
|
-
if tool is not None:
|
49
|
-
self.mcp.add_tool(tool)
|
50
|
-
|
51
|
-
|
52
|
-
class AdataState:
|
53
|
-
def __init__(self, add_adtypes=None):
|
54
|
-
self.adata_dic = {"exp": {}, "activity": {}, "cnv": {}, "splicing": {}}
|
55
|
-
if isinstance(add_adtypes, str):
|
56
|
-
self.adata_dic[add_adtypes] = {}
|
57
|
-
elif isinstance(add_adtypes, Iterable):
|
58
|
-
self.adata_dic.update({adtype: {} for adtype in add_adtypes})
|
59
|
-
self.active_id = None
|
60
|
-
self.metadatWa = {}
|
61
|
-
self.cr_kernel = {}
|
62
|
-
self.cr_estimator = {}
|
63
|
-
|
64
|
-
def get_adata(self, sampleid=None, adtype="exp", adinfo=None):
|
65
|
-
if adinfo is not None:
|
66
|
-
kwargs = adinfo.model_dump()
|
67
|
-
sampleid = kwargs.get("sampleid", None)
|
68
|
-
adtype = kwargs.get("adtype", "exp")
|
69
|
-
try:
|
70
|
-
if self.active_id is None:
|
71
|
-
return None
|
72
|
-
sampleid = sampleid or self.active_id
|
73
|
-
return self.adata_dic[adtype][sampleid]
|
74
|
-
except KeyError as e:
|
75
|
-
raise KeyError(f"Key {e} not found in adata_dic[{adtype}].Please check the sampleid or adtype.")
|
76
|
-
except Exception as e:
|
77
|
-
raise Exception(f"fuck {e} {type(e)}")
|
78
|
-
|
79
|
-
def set_adata(self, adata, sampleid=None, sdtype="exp", adinfo=None):
|
80
|
-
if adinfo is not None:
|
81
|
-
kwargs = adinfo.model_dump()
|
82
|
-
sampleid = kwargs.get("sampleid", None)
|
83
|
-
sdtype = kwargs.get("adtype", "exp")
|
84
|
-
sampleid = sampleid or self.active_id
|
85
|
-
if sdtype not in self.adata_dic:
|
86
|
-
self.adata_dic[sdtype] = {}
|
87
|
-
self.adata_dic[sdtype][sampleid] = adata
|
88
|
-
|
89
|
-
|
90
|
-
class BaseMCPManager:
|
91
|
-
"""Base class for MCP module management."""
|
92
|
-
|
93
|
-
def __init__(self,
|
94
|
-
name: str,
|
95
|
-
include_modules: Optional[List[str]] = None,
|
96
|
-
exclude_modules: Optional[List[str]] = None,
|
97
|
-
include_tools: Optional[List[str]] = None,
|
98
|
-
exclude_tools: Optional[List[str]] = None,
|
99
|
-
):
|
100
|
-
"""
|
101
|
-
Initialize BaseMCPManager with optional module filtering.
|
102
|
-
|
103
|
-
Args:
|
104
|
-
name (str): Name of the MCP server
|
105
|
-
include_modules (List[str], optional): List of module names to include. If None, all modules are included.
|
106
|
-
exclude_modules (List[str], optional): List of module names to exclude. If None, no modules are excluded.
|
107
|
-
include_tools (List[str], optional): List of tool names to include. If None, all tools are included.
|
108
|
-
exclude_tools (List[str], optional): List of tool names to exclude. If None, no tools are excluded.
|
109
|
-
"""
|
110
|
-
self.ads = AdataState()
|
111
|
-
self.mcp = FastMCP(name, lifespan=self.adata_lifespan)
|
112
|
-
self.include_modules = include_modules
|
113
|
-
self.exclude_modules = exclude_modules
|
114
|
-
self.include_tools = include_tools
|
115
|
-
self.exclude_tools = exclude_tools
|
116
|
-
self.available_modules = {}
|
117
|
-
self._init_modules()
|
118
|
-
self._register_modules()
|
119
|
-
|
120
|
-
def _init_modules(self):
|
121
|
-
"""Initialize available modules. To be implemented by subclasses."""
|
122
|
-
raise NotImplementedError("Subclasses must implement _init_modules")
|
123
|
-
|
124
|
-
def _register_modules(self):
|
125
|
-
"""Register modules based on include/exclude filters."""
|
126
|
-
# Filter modules based on include/exclude lists
|
127
|
-
if self.include_modules is not None:
|
128
|
-
self.available_modules = {k: v for k, v in self.available_modules.items() if k in self.include_modules}
|
129
|
-
|
130
|
-
if self.exclude_modules is not None:
|
131
|
-
self.available_modules = {k: v for k, v in self.available_modules.items() if k not in self.exclude_modules}
|
132
|
-
|
133
|
-
# Register each module
|
134
|
-
for module_name, mcpi in self.available_modules.items():
|
135
|
-
if isinstance(mcpi, FastMCP):
|
136
|
-
if self.include_tools is not None and module_name in self.include_tools:
|
137
|
-
mcpi = filter_tools(mcpi, include_tools= self.include_tools[module_name])
|
138
|
-
if self.exclude_tools is not None and module_name in self.exclude_tools:
|
139
|
-
mcpi = filter_tools(mcpi, exclude_tools=self.exclude_tools[module_name])
|
140
|
-
|
141
|
-
asyncio.run(self.mcp.import_server(module_name, mcpi))
|
142
|
-
else:
|
143
|
-
asyncio.run(self.mcp.import_server(module_name, mcpi().mcp))
|
144
|
-
|
145
|
-
@asynccontextmanager
|
146
|
-
async def adata_lifespan(self, server: FastMCP) -> AsyncIterator[Any]:
|
147
|
-
"""Context manager for AdataState lifecycle."""
|
148
|
-
yield self.ads
|
@@ -1,24 +0,0 @@
|
|
1
|
-
scmcp_shared/__init__.py,sha256=5DHi9fyCf-CfX64oAsNqmkR27WRUetIg-NfPin0QKBw,24
|
2
|
-
scmcp_shared/agent.py,sha256=tWGAyOwdg3oTfYFquGPompUZMHSWqW3VQvEymywaE0o,854
|
3
|
-
scmcp_shared/cli.py,sha256=8Am2zdn1z_gle6Jz-JQkvK-6_mOB6BFhfK05A0pNpkc,5032
|
4
|
-
scmcp_shared/logging_config.py,sha256=eCuLuyxMmbj8A1E0VqYWoKA5JPTSbo6cmjS4LOyd0RQ,872
|
5
|
-
scmcp_shared/util.py,sha256=8_c6WPNpYNoxKpq6tQCC4elCQkWyk3rH-hTe1sqff4A,9535
|
6
|
-
scmcp_shared/schema/__init__.py,sha256=Kwkc7kPLjExOlxt1sWEy_5qa96MvOS8sNCMlZa6yRg8,737
|
7
|
-
scmcp_shared/schema/io.py,sha256=-wgL2NQBXwAcojid8rF2Y46GsKL7H6JiheKE6frMotw,4638
|
8
|
-
scmcp_shared/schema/pl.py,sha256=7AFXgqb2GAlmeXS6m3IdJgThLz6-FTDBmrJqoAi8j98,29612
|
9
|
-
scmcp_shared/schema/pp.py,sha256=WtaugFLP9vfusBZQCXGAYPmYu-5yWHC2wJTSZutS1XM,21674
|
10
|
-
scmcp_shared/schema/tl.py,sha256=FEZpr218eaQ8rUp5EJzbjyw1ejqCX4Shsf9CumaKs8A,34425
|
11
|
-
scmcp_shared/schema/tool.py,sha256=U-VFrovAEr2FklS8cSJPimfhTSTHmskmLfCO7Ee0s2o,267
|
12
|
-
scmcp_shared/schema/util.py,sha256=fMZxTNf9Bv_xDzrW7YK08q0tCoC3L7ofqPY0K2ykG8k,4824
|
13
|
-
scmcp_shared/server/__init__.py,sha256=4KE2Y_gDenF0ZyTGicQW0fTgJfMIQYZfpRP4hQ4rFYs,416
|
14
|
-
scmcp_shared/server/auto.py,sha256=32SC5nC0N7JuPsDSMFRvojaPmUbpybwY1aCOrQMzaL4,1781
|
15
|
-
scmcp_shared/server/base.py,sha256=I90L_DNdCv--VOIHDRjdVehq-3iBm9mV0Sc9OU7GAXQ,6426
|
16
|
-
scmcp_shared/server/io.py,sha256=kGBbtd3ltj0ypd0kgMy1l2zT2AVf5yXCHAebQR-ZtUA,4033
|
17
|
-
scmcp_shared/server/pl.py,sha256=GVu7GQKW67jz3ig43HLX0d2cjGDPJDXdxMSf6b72kAA,15558
|
18
|
-
scmcp_shared/server/pp.py,sha256=UCzogVULgSs8JTR8PiybroGlrm2QvNRrbeAYj7ujr3E,16297
|
19
|
-
scmcp_shared/server/tl.py,sha256=LeKcR6F4PNRHZomlD3-igrF44YBQW6Kg7h75psGl9c0,19112
|
20
|
-
scmcp_shared/server/util.py,sha256=b6gG1dfXe5GdhM4yUtpxBqx1gMRyQVu3dytmzJeD9zs,12590
|
21
|
-
scmcp_shared-0.4.0.dist-info/METADATA,sha256=CKca84Vmtqc5h06oOH6lxqYVDCeVNJpGypX03Y-O1iI,2435
|
22
|
-
scmcp_shared-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
23
|
-
scmcp_shared-0.4.0.dist-info/licenses/LICENSE,sha256=YNr1hpea195yq-wGtB8j-2dGtt7A5G00WENmxa7JGco,1495
|
24
|
-
scmcp_shared-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|