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
@@ -1,21 +1,21 @@
1
- import os
2
- import inspect
3
- from pathlib import Path
4
- import scanpy as sc
5
- from fastmcp import FastMCP , Context
6
1
  from fastmcp.exceptions import ToolError
7
2
  from fastmcp.tools.tool import Tool
8
- from ..schema.util import *
9
- from ..schema import AdataInfo
10
- from ..util import filter_args, forward_request, get_ads, generate_msg,add_op_log
11
- from .base import BaseMCP
3
+ from scmcp_shared.schema.preset.util import *
4
+ from scmcp_shared.schema.preset import AdataInfo
5
+ from scmcp_shared.util import forward_request, get_ads, add_op_log
6
+ from scmcp_shared.mcp_base import BaseMCP
12
7
 
13
8
 
14
9
  class ScanpyUtilMCP(BaseMCP):
15
- def __init__(self, include_tools: list = None, exclude_tools: list = None, AdataInfo = AdataInfo):
10
+ def __init__(
11
+ self,
12
+ include_tools: list = None,
13
+ exclude_tools: list = None,
14
+ AdataInfo=AdataInfo,
15
+ ):
16
16
  """
17
17
  Initialize ScanpyUtilMCP with optional tool filtering.
18
-
18
+
19
19
  Args:
20
20
  include_tools (list, optional): List of tool names to include. If None, all tools are included.
21
21
  exclude_tools (list, optional): List of tool names to exclude. If None, no tools are excluded.
@@ -23,21 +23,23 @@ class ScanpyUtilMCP(BaseMCP):
23
23
  """
24
24
  super().__init__("SCMCP-Util-Server", include_tools, exclude_tools, AdataInfo)
25
25
 
26
-
27
26
  def _tool_query_op_log(self):
28
- def _query_op_log(request: QueryOpLogParams, adinfo: self.AdataInfo=self.AdataInfo()):
27
+ def _query_op_log(
28
+ request: QueryOpLogParam, adinfo: self.AdataInfo = self.AdataInfo()
29
+ ):
29
30
  """Query the adata operation log"""
30
31
  adata = get_ads().get_adata(adinfo=adinfo)
31
32
  op_dic = adata.uns["operation"]["op"]
32
- opids = adata.uns["operation"]["opid"][-request.n:]
33
+ opids = adata.uns["operation"]["opid"][-request.n :]
33
34
  op_list = []
34
35
  for opid in opids:
35
36
  op_list.append(op_dic[opid])
36
37
  return op_list
37
- return Tool.from_function(_query_op_log, name="query_op_log")
38
+
39
+ return Tool.from_function(_query_op_log, name="query_op_log", enabled=True)
38
40
 
39
41
  def _tool_mark_var(self):
40
- def _mark_var(request: MarkVarParams, adinfo: self.AdataInfo=self.AdataInfo()):
42
+ def _mark_var(request: MarkVarParam, adinfo: self.AdataInfo = self.AdataInfo()):
41
43
  """
42
44
  Determine if each gene meets specific conditions and store results in adata.var as boolean values.
43
45
  For example: mitochondrion genes startswith MT-.
@@ -54,14 +56,20 @@ class ScanpyUtilMCP(BaseMCP):
54
56
  patterns = request.patterns
55
57
  if gene_class is not None:
56
58
  if gene_class == "mitochondrion":
57
- adata.var["mt"] = adata.var_names.str.startswith(('MT-', 'Mt','mt-'))
59
+ adata.var["mt"] = adata.var_names.str.startswith(
60
+ ("MT-", "Mt", "mt-")
61
+ )
58
62
  var_name = "mt"
59
63
  elif gene_class == "ribosomal":
60
- adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL", "Rps", "Rpl"))
64
+ adata.var["ribo"] = adata.var_names.str.startswith(
65
+ ("RPS", "RPL", "Rps", "Rpl")
66
+ )
61
67
  var_name = "ribo"
62
68
  elif gene_class == "hemoglobin":
63
- adata.var["hb"] = adata.var_names.str.contains("^HB[^(P)]", case=False)
64
- var_name = "hb"
69
+ adata.var["hb"] = adata.var_names.str.contains(
70
+ "^HB[^(P)]", case=False
71
+ )
72
+ var_name = "hb"
65
73
  elif pattern_type is not None and patterns is not None:
66
74
  if pattern_type == "startswith":
67
75
  adata.var[var_name] = adata.var_names.str.startswith(patterns)
@@ -70,25 +78,39 @@ class ScanpyUtilMCP(BaseMCP):
70
78
  elif pattern_type == "contains":
71
79
  adata.var[var_name] = adata.var_names.str.contains(patterns)
72
80
  else:
73
- raise ValueError(f"Did not support pattern_type: {pattern_type}")
81
+ raise ValueError(
82
+ f"Did not support pattern_type: {pattern_type}"
83
+ )
74
84
  else:
75
- raise ValueError(f"Please provide validated parameter")
76
-
77
- res = {var_name: adata.var[var_name].value_counts().to_dict(), "msg": f"add '{var_name}' column in adata.var"}
78
- func_kwargs = {"var_name": var_name, "gene_class": gene_class, "pattern_type": pattern_type, "patterns": patterns}
85
+ raise ValueError("Please provide validated parameter")
86
+
87
+ res = {
88
+ var_name: adata.var[var_name].value_counts().to_dict(),
89
+ "msg": f"add '{var_name}' column in adata.var",
90
+ }
91
+ func_kwargs = {
92
+ "var_name": var_name,
93
+ "gene_class": gene_class,
94
+ "pattern_type": pattern_type,
95
+ "patterns": patterns,
96
+ }
79
97
  add_op_log(adata, "mark_var", func_kwargs, adinfo)
80
98
  return res
81
99
  except ToolError as e:
82
100
  raise ToolError(e)
83
101
  except Exception as e:
84
- if hasattr(e, '__context__') and e.__context__:
102
+ if hasattr(e, "__context__") and e.__context__:
85
103
  raise ToolError(e.__context__)
86
104
  else:
87
105
  raise ToolError(e)
88
- return Tool.from_function(_mark_var, name="mark_var")
106
+
107
+ return Tool.from_function(_mark_var, name="mark_var", enabled=True)
89
108
 
90
109
  def _tool_list_var(self):
91
- def _list_var(request: ListVarParams=ListVarParams(), adinfo: self.AdataInfo=self.AdataInfo()):
110
+ def _list_var(
111
+ request: ListVarParam = ListVarParam(),
112
+ adinfo: self.AdataInfo = self.AdataInfo(),
113
+ ):
92
114
  """List key columns in adata.var. It should be called for checking when other tools need var key column names as input."""
93
115
  try:
94
116
  result = forward_request("ul_list_var", request, adinfo)
@@ -101,19 +123,20 @@ class ScanpyUtilMCP(BaseMCP):
101
123
  except ToolError as e:
102
124
  raise ToolError(e)
103
125
  except Exception as e:
104
- if hasattr(e, '__context__') and e.__context__:
126
+ if hasattr(e, "__context__") and e.__context__:
105
127
  raise ToolError(e.__context__)
106
128
  else:
107
129
  raise ToolError(e)
108
- return Tool.from_function(_list_var, name="list_var")
130
+
131
+ return Tool.from_function(_list_var, name="list_var", enabled=True)
109
132
 
110
133
  def _tool_list_obs(self):
111
- def _list_obs(request: ListObsParams, adinfo: self.AdataInfo=self.AdataInfo()):
134
+ def _list_obs(request: ListObsParam, adinfo: self.AdataInfo = self.AdataInfo()):
112
135
  """List key columns in adata.obs. It should be called before other tools need obs key column names input."""
113
136
  try:
114
137
  result = forward_request("ul_list_obs", request, adinfo)
115
138
  if result is not None:
116
- return result
139
+ return result
117
140
  adata = get_ads().get_adata(adinfo=adinfo)
118
141
  columns = list(adata.obs.columns)
119
142
  add_op_log(adata, "list_obs", {}, adinfo)
@@ -121,19 +144,22 @@ class ScanpyUtilMCP(BaseMCP):
121
144
  except ToolError as e:
122
145
  raise ToolError(e)
123
146
  except Exception as e:
124
- if hasattr(e, '__context__') and e.__context__:
147
+ if hasattr(e, "__context__") and e.__context__:
125
148
  raise ToolError(e.__context__)
126
149
  else:
127
150
  raise ToolError(e)
128
- return Tool.from_function(_list_obs, name="list_obs")
151
+
152
+ return Tool.from_function(_list_obs, name="list_obs", enabled=True)
129
153
 
130
154
  def _tool_check_var(self):
131
- def _check_var(request: VarNamesParams, adinfo: self.AdataInfo=self.AdataInfo()):
155
+ def _check_var(
156
+ request: VarNamesParam, adinfo: self.AdataInfo = self.AdataInfo()
157
+ ):
132
158
  """Check if genes/variables exist in adata.var_names. This tool should be called before gene expression visualizations or color by genes."""
133
159
  try:
134
160
  result = forward_request("ul_check_var", request, adinfo)
135
161
  if result is not None:
136
- return result
162
+ return result
137
163
  adata = get_ads().get_adata(adinfo=adinfo)
138
164
  var_names = request.var_names
139
165
  if adata.raw is not None:
@@ -146,14 +172,17 @@ class ScanpyUtilMCP(BaseMCP):
146
172
  except ToolError as e:
147
173
  raise ToolError(e)
148
174
  except Exception as e:
149
- if hasattr(e, '__context__') and e.__context__:
175
+ if hasattr(e, "__context__") and e.__context__:
150
176
  raise ToolError(e.__context__)
151
177
  else:
152
178
  raise ToolError(e)
153
- return Tool.from_function(_check_var, name="check_var")
179
+
180
+ return Tool.from_function(_check_var, name="check_var", enabled=True)
154
181
 
155
182
  def _tool_merge_adata(self):
156
- def _merge_adata(request: ConcatBaseParams, adinfo: self.AdataInfo=self.AdataInfo()):
183
+ def _merge_adata(
184
+ request: ConcatBaseParam, adinfo: self.AdataInfo = self.AdataInfo()
185
+ ):
157
186
  """Merge multiple adata objects."""
158
187
  try:
159
188
  result = forward_request("ul_merge_adata", request, adinfo)
@@ -161,96 +190,124 @@ class ScanpyUtilMCP(BaseMCP):
161
190
  return result
162
191
  ads = get_ads()
163
192
  adata = ads.get_adata(adinfo=adinfo)
164
- kwargs = {k: v for k, v in request.model_dump().items() if v is not None}
193
+ kwargs = {
194
+ k: v for k, v in request.model_dump().items() if v is not None
195
+ }
165
196
  merged_adata = adata.concat(ads.adata_dic, **kwargs)
166
197
  ads.adata_dic[dtype] = {}
167
198
  ads.active_id = "merged_adata"
168
199
  add_op_log(merged_adata, ad.concat, kwargs, adinfo)
169
200
  ads.adata_dic[ads.active_id] = merged_adata
170
- return {"status": "success", "message": "Successfully merged all AnnData objects"}
201
+ return {
202
+ "status": "success",
203
+ "message": "Successfully merged all AnnData objects",
204
+ }
171
205
  except ToolError as e:
172
206
  raise ToolError(e)
173
207
  except Exception as e:
174
- if hasattr(e, '__context__') and e.__context__:
208
+ if hasattr(e, "__context__") and e.__context__:
175
209
  raise ToolError(e.__context__)
176
210
  else:
177
211
  raise ToolError(e)
178
- return Tool.from_function(_merge_adata, name="merge_adata")
212
+
213
+ return Tool.from_function(_merge_adata, name="merge_adata", enabled=True)
179
214
 
180
215
  def _tool_set_dpt_iroot(self):
181
- def _set_dpt_iroot(request: DPTIROOTParams, adinfo: self.AdataInfo=self.AdataInfo()):
216
+ def _set_dpt_iroot(
217
+ request: DPTIROOTParam, adinfo: self.AdataInfo = self.AdataInfo()
218
+ ):
182
219
  """Set the iroot cell"""
183
220
  try:
184
221
  result = forward_request("ul_set_dpt_iroot", request, adinfo)
185
222
  if result is not None:
186
- return result
223
+ return result
187
224
  adata = get_ads().get_adata(adinfo=adinfo)
188
225
  diffmap_key = request.diffmap_key
189
226
  dimension = request.dimension
190
227
  direction = request.direction
191
228
  if diffmap_key not in adata.obsm:
192
- raise ValueError(f"Diffusion map key '{diffmap_key}' not found in adata.obsm")
229
+ raise ValueError(
230
+ f"Diffusion map key '{diffmap_key}' not found in adata.obsm"
231
+ )
193
232
  if direction == "min":
194
233
  adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmin()
195
- else:
234
+ else:
196
235
  adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmax()
197
-
198
- func_kwargs = {"diffmap_key": diffmap_key, "dimension": dimension, "direction": direction}
236
+
237
+ func_kwargs = {
238
+ "diffmap_key": diffmap_key,
239
+ "dimension": dimension,
240
+ "direction": direction,
241
+ }
199
242
  add_op_log(adata, "set_dpt_iroot", func_kwargs, adinfo)
200
-
201
- return {"status": "success", "message": f"Successfully set root cell for DPT using {direction} of dimension {dimension}"}
243
+
244
+ return {
245
+ "status": "success",
246
+ "message": f"Successfully set root cell for DPT using {direction} of dimension {dimension}",
247
+ }
202
248
  except ToolError as e:
203
249
  raise ToolError(e)
204
250
  except Exception as e:
205
- if hasattr(e, '__context__') and e.__context__:
251
+ if hasattr(e, "__context__") and e.__context__:
206
252
  raise ToolError(e.__context__)
207
253
  else:
208
254
  raise ToolError(e)
209
- return Tool.from_function(_set_dpt_iroot, name="set_dpt_iroot")
255
+
256
+ return Tool.from_function(_set_dpt_iroot, name="set_dpt_iroot", enabled=True)
210
257
 
211
258
  def _tool_add_layer(self):
212
- def _add_layer(request: AddLayerParams, adinfo: self.AdataInfo=self.AdataInfo()):
259
+ def _add_layer(
260
+ request: AddLayerParam, adinfo: self.AdataInfo = self.AdataInfo()
261
+ ):
213
262
  """Add a layer to the AnnData object."""
214
263
  try:
215
264
  result = forward_request("ul_add_layer", request, adinfo)
216
265
  if result is not None:
217
- return result
266
+ return result
218
267
  adata = get_ads().get_adata(adinfo=adinfo)
219
268
  layer_name = request.layer_name
220
-
269
+
221
270
  # Check if layer already exists
222
271
  if layer_name in adata.layers:
223
- raise ValueError(f"Layer '{layer_name}' already exists in adata.layers")
272
+ raise ValueError(
273
+ f"Layer '{layer_name}' already exists in adata.layers"
274
+ )
224
275
  # Add the data as a new layer
225
276
  adata.layers[layer_name] = adata.X.copy()
226
277
 
227
278
  func_kwargs = {"layer_name": layer_name}
228
279
  add_op_log(adata, "add_layer", func_kwargs, adinfo)
229
-
280
+
230
281
  return {
231
- "status": "success",
232
- "message": f"Successfully added layer '{layer_name}' to adata.layers"
282
+ "status": "success",
283
+ "message": f"Successfully added layer '{layer_name}' to adata.layers",
233
284
  }
234
285
  except ToolError as e:
235
286
  raise ToolError(e)
236
287
  except Exception as e:
237
- if hasattr(e, '__context__') and e.__context__:
288
+ if hasattr(e, "__context__") and e.__context__:
238
289
  raise ToolError(e.__context__)
239
290
  else:
240
291
  raise ToolError(e)
241
- return Tool.from_function(_add_layer, name="add_layer")
292
+
293
+ return Tool.from_function(_add_layer, name="add_layer", enabled=True)
242
294
 
243
295
  def _tool_check_samples(self):
244
- def _check_samples(request: None, adinfo: self.AdataInfo=self.AdataInfo()):
296
+ def _check_samples(request: None, adinfo: self.AdataInfo = self.AdataInfo()):
245
297
  """check the stored samples"""
246
298
  try:
247
299
  ads = get_ads()
248
- return {"sampleid": [list(ads.adata_dic[dk].keys()) for dk in ads.adata_dic.keys()]}
300
+ return {
301
+ "sampleid": [
302
+ list(ads.adata_dic[dk].keys()) for dk in ads.adata_dic.keys()
303
+ ]
304
+ }
249
305
  except ToolError as e:
250
306
  raise ToolError(e)
251
307
  except Exception as e:
252
- if hasattr(e, '__context__') and e.__context__:
308
+ if hasattr(e, "__context__") and e.__context__:
253
309
  raise ToolError(e.__context__)
254
310
  else:
255
311
  raise ToolError(e)
256
- return Tool.from_function(_check_samples, name="check_samples")
312
+
313
+ return Tool.from_function(_check_samples, name="check_samples", enabled=True)
@@ -0,0 +1,13 @@
1
+ from fastmcp import FastMCP
2
+ from ..agent import rag_agent
3
+ from pydantic import Field
4
+
5
+ rag_mcp = FastMCP("RAG-Server")
6
+
7
+
8
+ @rag_mcp.tool(tags={"rag"})
9
+ def retrieve_knowledge(
10
+ task: str = Field(description="The tasks or questions that needs to be solved"),
11
+ ):
12
+ """search function and parameters that can be used to solve the user's tasks or questions"""
13
+ return rag_agent(task, software="scmcp")