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.
Files changed (35) hide show
  1. scmcp_shared/__init__.py +1 -3
  2. scmcp_shared/agent.py +38 -21
  3. scmcp_shared/backend.py +44 -0
  4. scmcp_shared/cli.py +75 -46
  5. scmcp_shared/kb.py +139 -0
  6. scmcp_shared/logging_config.py +6 -8
  7. scmcp_shared/mcp_base.py +184 -0
  8. scmcp_shared/schema/io.py +101 -59
  9. scmcp_shared/schema/pl.py +386 -490
  10. scmcp_shared/schema/pp.py +514 -265
  11. scmcp_shared/schema/preset/__init__.py +15 -0
  12. scmcp_shared/schema/preset/io.py +103 -0
  13. scmcp_shared/schema/preset/pl.py +843 -0
  14. scmcp_shared/schema/preset/pp.py +616 -0
  15. scmcp_shared/schema/preset/tl.py +917 -0
  16. scmcp_shared/schema/preset/util.py +123 -0
  17. scmcp_shared/schema/tl.py +355 -407
  18. scmcp_shared/schema/util.py +57 -72
  19. scmcp_shared/server/__init__.py +5 -10
  20. scmcp_shared/server/auto.py +15 -11
  21. scmcp_shared/server/code.py +3 -0
  22. scmcp_shared/server/preset/__init__.py +14 -0
  23. scmcp_shared/server/{io.py → preset/io.py} +26 -22
  24. scmcp_shared/server/{pl.py → preset/pl.py} +162 -78
  25. scmcp_shared/server/{pp.py → preset/pp.py} +123 -65
  26. scmcp_shared/server/{tl.py → preset/tl.py} +142 -79
  27. scmcp_shared/server/{util.py → preset/util.py} +123 -66
  28. scmcp_shared/server/rag.py +13 -0
  29. scmcp_shared/util.py +109 -38
  30. {scmcp_shared-0.4.0.dist-info → scmcp_shared-0.6.0.dist-info}/METADATA +6 -2
  31. scmcp_shared-0.6.0.dist-info/RECORD +35 -0
  32. scmcp_shared/server/base.py +0 -148
  33. scmcp_shared-0.4.0.dist-info/RECORD +0 -24
  34. {scmcp_shared-0.4.0.dist-info → scmcp_shared-0.6.0.dist-info}/WHEEL +0 -0
  35. {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
- kwargs_str = str(new_kwargs)
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, 'savefig'): # if Figure
80
+ elif hasattr(axes, "savefig"): # if Figure
80
81
  axes.savefig(file_path)
81
- elif hasattr(axes, 'save'): # for plotnine.ggplot.ggplot
82
+ elif hasattr(axes, "save"): # for plotnine.ggplot.ggplot
82
83
  axes.save(file_path)
83
84
  else:
84
- raise ValueError(f"axes must be a Axes or plotnine object, but got {type(axes)}")
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(content={"error": "figure not found"}, media_type="application/json")
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
- server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
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, '__context__') and e.__context__:
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, '__context__') and e.__context__:
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(adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm, uns=adata.uns)
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 {"sampleid": adinfo.sampleid or ads.active_id, "adtype": adinfo.adtype, "adata": adata}
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['SCMCP_LOG_FILE'] = log_file
265
+ os.environ["SCMCP_LOG_FILE"] = log_file
250
266
  if forward is not None:
251
- os.environ['SCMCP_FORWARD'] = forward
252
- os.environ['SCMCP_TRANSPORT'] = transport
253
- os.environ['SCMCP_HOST'] = host
254
- os.environ['SCMCP_PORT'] = str(port)
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
- def _update_args(mcp, func, args_dic : dict):
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"]["request"].setdefault(pk, {})
270
- mcp._tool_manager._tools[func].parameters["properties"]["request"][pk][args] = v
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 : dict):
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(f"sampleid mismatch: {sampleid} != {adata.uns['scmcp_sampleid']}")
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.4.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: instructor>=1.8.3
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,,
@@ -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,,