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
@@ -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
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from .
|
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__(
|
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(
|
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
|
-
|
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:
|
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(
|
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(
|
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(
|
64
|
-
|
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(
|
81
|
+
raise ValueError(
|
82
|
+
f"Did not support pattern_type: {pattern_type}"
|
83
|
+
)
|
74
84
|
else:
|
75
|
-
raise ValueError(
|
76
|
-
|
77
|
-
res = {
|
78
|
-
|
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,
|
102
|
+
if hasattr(e, "__context__") and e.__context__:
|
85
103
|
raise ToolError(e.__context__)
|
86
104
|
else:
|
87
105
|
raise ToolError(e)
|
88
|
-
|
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(
|
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,
|
126
|
+
if hasattr(e, "__context__") and e.__context__:
|
105
127
|
raise ToolError(e.__context__)
|
106
128
|
else:
|
107
129
|
raise ToolError(e)
|
108
|
-
|
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:
|
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,
|
147
|
+
if hasattr(e, "__context__") and e.__context__:
|
125
148
|
raise ToolError(e.__context__)
|
126
149
|
else:
|
127
150
|
raise ToolError(e)
|
128
|
-
|
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(
|
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,
|
175
|
+
if hasattr(e, "__context__") and e.__context__:
|
150
176
|
raise ToolError(e.__context__)
|
151
177
|
else:
|
152
178
|
raise ToolError(e)
|
153
|
-
|
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(
|
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 = {
|
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 {
|
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,
|
208
|
+
if hasattr(e, "__context__") and e.__context__:
|
175
209
|
raise ToolError(e.__context__)
|
176
210
|
else:
|
177
211
|
raise ToolError(e)
|
178
|
-
|
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(
|
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(
|
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 = {
|
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 {
|
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,
|
251
|
+
if hasattr(e, "__context__") and e.__context__:
|
206
252
|
raise ToolError(e.__context__)
|
207
253
|
else:
|
208
254
|
raise ToolError(e)
|
209
|
-
|
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(
|
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(
|
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,
|
288
|
+
if hasattr(e, "__context__") and e.__context__:
|
238
289
|
raise ToolError(e.__context__)
|
239
290
|
else:
|
240
291
|
raise ToolError(e)
|
241
|
-
|
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 {
|
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,
|
308
|
+
if hasattr(e, "__context__") and e.__context__:
|
253
309
|
raise ToolError(e.__context__)
|
254
310
|
else:
|
255
311
|
raise ToolError(e)
|
256
|
-
|
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")
|