scmcp-shared 0.2.5__py3-none-any.whl → 0.3.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 -1
- scmcp_shared/cli.py +96 -0
- scmcp_shared/schema/__init__.py +9 -0
- scmcp_shared/schema/io.py +0 -4
- scmcp_shared/schema/tl.py +0 -5
- scmcp_shared/server/__init__.py +11 -49
- scmcp_shared/server/base.py +153 -0
- scmcp_shared/server/io.py +70 -70
- scmcp_shared/server/pl.py +287 -338
- scmcp_shared/server/pp.py +319 -373
- scmcp_shared/server/tl.py +392 -441
- scmcp_shared/server/util.py +240 -250
- scmcp_shared/util.py +82 -17
- {scmcp_shared-0.2.5.dist-info → scmcp_shared-0.3.0.dist-info}/METADATA +1 -1
- scmcp_shared-0.3.0.dist-info/RECORD +21 -0
- scmcp_shared-0.2.5.dist-info/RECORD +0 -19
- {scmcp_shared-0.2.5.dist-info → scmcp_shared-0.3.0.dist-info}/WHEEL +0 -0
- {scmcp_shared-0.2.5.dist-info → scmcp_shared-0.3.0.dist-info}/licenses/LICENSE +0 -0
scmcp_shared/server/util.py
CHANGED
@@ -5,257 +5,247 @@ import scanpy as sc
|
|
5
5
|
from fastmcp import FastMCP , Context
|
6
6
|
from fastmcp.exceptions import ToolError
|
7
7
|
from ..schema.util import *
|
8
|
-
from ..schema import AdataModel
|
8
|
+
from ..schema import AdataModel, AdataInfo
|
9
9
|
from ..util import filter_args, forward_request, get_ads, generate_msg,add_op_log
|
10
|
+
from .base import BaseMCP
|
10
11
|
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
async def query_op_log(
|
17
|
-
request: QueryOpLogModel = QueryOpLogModel(),
|
18
|
-
adinfo: AdataModel = AdataModel()
|
19
|
-
):
|
20
|
-
"""Query the adata operation log"""
|
21
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
22
|
-
op_dic = adata.uns["operation"]["op"]
|
23
|
-
opids = adata.uns["operation"]["opid"][-n:]
|
24
|
-
op_list = []
|
25
|
-
for opid in opids:
|
26
|
-
op_list.append(op_dic[opid])
|
27
|
-
return op_list
|
28
|
-
|
29
|
-
|
30
|
-
@ul_mcp.tool()
|
31
|
-
async def mark_var(
|
32
|
-
request: MarkVarModel = MarkVarModel(),
|
33
|
-
adinfo: AdataModel = AdataModel()
|
34
|
-
):
|
35
|
-
"""
|
36
|
-
Determine if each gene meets specific conditions and store results in adata.var as boolean values.
|
37
|
-
For example: mitochondrion genes startswith MT-.
|
38
|
-
The tool should be called first when calculate quality control metrics for mitochondrion, ribosomal, harhemoglobin genes, or other qc_vars.
|
39
|
-
"""
|
40
|
-
try:
|
41
|
-
result = await forward_request("ul_mark_var", request, adinfo)
|
42
|
-
if result is not None:
|
43
|
-
return result
|
44
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
45
|
-
var_name = request.var_name
|
46
|
-
gene_class = request.gene_class
|
47
|
-
pattern_type = request.pattern_type
|
48
|
-
patterns = request.patterns
|
49
|
-
if gene_class is not None:
|
50
|
-
if gene_class == "mitochondrion":
|
51
|
-
adata.var["mt"] = adata.var_names.str.startswith(('MT-', 'Mt','mt-'))
|
52
|
-
var_name = "mt"
|
53
|
-
elif gene_class == "ribosomal":
|
54
|
-
adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL", "Rps", "Rpl"))
|
55
|
-
var_name = "ribo"
|
56
|
-
elif gene_class == "hemoglobin":
|
57
|
-
adata.var["hb"] = adata.var_names.str.contains("^HB[^(P)]", case=False)
|
58
|
-
var_name = "hb"
|
59
|
-
elif pattern_type is not None and patterns is not None:
|
60
|
-
if pattern_type == "startswith":
|
61
|
-
adata.var[var_name] = adata.var_names.str.startswith(patterns)
|
62
|
-
elif pattern_type == "endswith":
|
63
|
-
adata.var[var_name] = adata.var_names.str.endswith(patterns)
|
64
|
-
elif pattern_type == "contains":
|
65
|
-
adata.var[var_name] = adata.var_names.str.contains(patterns)
|
66
|
-
else:
|
67
|
-
raise ValueError(f"Did not support pattern_type: {pattern_type}")
|
68
|
-
else:
|
69
|
-
raise ValueError(f"Please provide validated parameter")
|
70
|
-
|
71
|
-
res = {var_name: adata.var[var_name].value_counts().to_dict(), "msg": f"add '{var_name}' column in adata.var"}
|
72
|
-
func_kwargs = {"var_name": var_name, "gene_class": gene_class, "pattern_type": pattern_type, "patterns": patterns}
|
73
|
-
add_op_log(adata, "mark_var", func_kwargs, adinfo)
|
74
|
-
return res
|
75
|
-
except ToolError as e:
|
76
|
-
raise ToolError(e)
|
77
|
-
except Exception as e:
|
78
|
-
if hasattr(e, '__context__') and e.__context__:
|
79
|
-
raise ToolError(e.__context__)
|
80
|
-
else:
|
81
|
-
raise ToolError(e)
|
82
|
-
|
83
|
-
|
84
|
-
@ul_mcp.tool()
|
85
|
-
async def list_var(
|
86
|
-
request: ListVarModel = ListVarModel(),
|
87
|
-
adinfo: AdataModel = AdataModel()
|
88
|
-
):
|
89
|
-
"""List key columns in adata.var. It should be called for checking when other tools need var key column names as input."""
|
90
|
-
try:
|
91
|
-
result = await forward_request("ul_list_var", request, adinfo)
|
92
|
-
if result is not None:
|
93
|
-
return result
|
94
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
95
|
-
columns = list(adata.var.columns)
|
96
|
-
add_op_log(adata, list_var, {}, adinfo)
|
97
|
-
return columns
|
98
|
-
except ToolError as e:
|
99
|
-
raise ToolError(e)
|
100
|
-
except Exception as e:
|
101
|
-
if hasattr(e, '__context__') and e.__context__:
|
102
|
-
raise ToolError(e.__context__)
|
103
|
-
else:
|
104
|
-
raise ToolError(e)
|
105
|
-
|
106
|
-
@ul_mcp.tool()
|
107
|
-
async def list_obs(
|
108
|
-
request: ListObsModel = ListObsModel(),
|
109
|
-
adinfo: AdataModel = AdataModel()
|
110
|
-
):
|
111
|
-
"""List key columns in adata.obs. It should be called before other tools need obs key column names input."""
|
112
|
-
try:
|
113
|
-
result = await forward_request("ul_list_obs", request, adinfo)
|
114
|
-
if result is not None:
|
115
|
-
return result
|
116
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
117
|
-
columns = list(adata.obs.columns)
|
118
|
-
add_op_log(adata, list_obs, {}, adinfo)
|
119
|
-
return columns
|
120
|
-
except ToolError as e:
|
121
|
-
raise ToolError(e)
|
122
|
-
except Exception as e:
|
123
|
-
if hasattr(e, '__context__') and e.__context__:
|
124
|
-
raise ToolError(e.__context__)
|
125
|
-
else:
|
126
|
-
raise ToolError(e)
|
127
|
-
|
128
|
-
@ul_mcp.tool()
|
129
|
-
async def check_var(
|
130
|
-
request: VarNamesModel = VarNamesModel(),
|
131
|
-
adinfo: AdataModel = AdataModel()
|
132
|
-
):
|
133
|
-
"""Check if genes/variables exist in adata.var_names. This tool should be called before gene expression visualizations or color by genes."""
|
134
|
-
try:
|
135
|
-
result = await forward_request("ul_check_var", request, adinfo)
|
136
|
-
if result is not None:
|
137
|
-
return result
|
138
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
139
|
-
var_names = request.var_names
|
140
|
-
result = {v: v in adata.var_names for v in var_names}
|
141
|
-
add_op_log(adata, check_var, {"var_names": var_names}, adinfo)
|
142
|
-
return result
|
143
|
-
except ToolError as e:
|
144
|
-
raise ToolError(e)
|
145
|
-
except Exception as e:
|
146
|
-
if hasattr(e, '__context__') and e.__context__:
|
147
|
-
raise ToolError(e.__context__)
|
148
|
-
else:
|
149
|
-
raise ToolError(e)
|
150
|
-
|
151
|
-
@ul_mcp.tool()
|
152
|
-
async def merge_adata(
|
153
|
-
request: ConcatBaseModel = ConcatBaseModel(),
|
154
|
-
adinfo: AdataModel = AdataModel()
|
155
|
-
):
|
156
|
-
"""Merge multiple adata objects."""
|
157
|
-
|
158
|
-
try:
|
159
|
-
result = await forward_request("ul_merge_adata", request, adinfo)
|
160
|
-
if result is not None:
|
161
|
-
return result
|
162
|
-
ads = get_ads()
|
163
|
-
adata = ads.get_adata(adinfo=adinfo)
|
164
|
-
kwargs = {k: v for k, v in request.model_dump().items() if v is not None}
|
165
|
-
merged_adata = adata.concat(list(ads.adata_dic[dtype].values()), **kwargs)
|
166
|
-
ads.adata_dic[dtype] = {}
|
167
|
-
ads.active_id = "merged_adata"
|
168
|
-
add_op_log(merged_adata, ad.concat, kwargs, adinfo)
|
169
|
-
ads.adata_dic[ads.active_id] = merged_adata
|
170
|
-
return {"status": "success", "message": "Successfully merged all AnnData objects"}
|
171
|
-
except ToolError as e:
|
172
|
-
raise ToolError(e)
|
173
|
-
except Exception as e:
|
174
|
-
if hasattr(e, '__context__') and e.__context__:
|
175
|
-
raise ToolError(e.__context__)
|
176
|
-
else:
|
177
|
-
raise ToolError(e)
|
178
|
-
|
179
|
-
|
180
|
-
@ul_mcp.tool()
|
181
|
-
async def set_dpt_iroot(
|
182
|
-
request: DPTIROOTModel,
|
183
|
-
adinfo: AdataModel = AdataModel()
|
184
|
-
):
|
185
|
-
"""Set the iroot cell"""
|
186
|
-
try:
|
187
|
-
result = await forward_request("ul_set_dpt_iroot", request, adinfo)
|
188
|
-
if result is not None:
|
189
|
-
return result
|
190
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
191
|
-
diffmap_key = request.diffmap_key
|
192
|
-
dimension = request.dimension
|
193
|
-
direction = request.direction
|
194
|
-
if diffmap_key not in adata.obsm:
|
195
|
-
raise ValueError(f"Diffusion map key '{diffmap_key}' not found in adata.obsm")
|
196
|
-
if direction == "min":
|
197
|
-
adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmin()
|
198
|
-
else:
|
199
|
-
adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmax()
|
200
|
-
|
201
|
-
func_kwargs = {"diffmap_key": diffmap_key, "dimension": dimension, "direction": direction}
|
202
|
-
add_op_log(adata, "set_dpt_iroot", func_kwargs, adinfo)
|
203
|
-
|
204
|
-
return {"status": "success", "message": f"Successfully set root cell for DPT using {direction} of dimension {dimension}"}
|
205
|
-
except ToolError as e:
|
206
|
-
raise ToolError(e)
|
207
|
-
except Exception as e:
|
208
|
-
if hasattr(e, '__context__') and e.__context__:
|
209
|
-
raise ToolError(e.__context__)
|
210
|
-
else:
|
211
|
-
raise ToolError(e)
|
212
|
-
|
213
|
-
@ul_mcp.tool()
|
214
|
-
async def add_layer(
|
215
|
-
request: AddLayerModel,
|
216
|
-
adinfo: AdataModel = AdataModel()
|
217
|
-
):
|
218
|
-
"""Add a layer to the AnnData object.
|
219
|
-
"""
|
220
|
-
try:
|
221
|
-
result = await forward_request("ul_add_layer", request, adinfo)
|
222
|
-
if result is not None:
|
223
|
-
return result
|
224
|
-
adata = get_ads().get_adata(adinfo=adinfo)
|
225
|
-
layer_name = request.layer_name
|
226
|
-
|
227
|
-
# Check if layer already exists
|
228
|
-
if layer_name in adata.layers:
|
229
|
-
raise ValueError(f"Layer '{layer_name}' already exists in adata.layers")
|
230
|
-
# Add the data as a new layer
|
231
|
-
adata.layers[layer_name] = adata.X.copy()
|
232
|
-
|
233
|
-
func_kwargs = {"layer_name": layer_name}
|
234
|
-
add_op_log(adata, "add_layer", func_kwargs, adinfo)
|
13
|
+
class ScanpyUtilMCP(BaseMCP):
|
14
|
+
def __init__(self, include_tools: list = None, exclude_tools: list = None, AdataInfo = AdataInfo):
|
15
|
+
"""
|
16
|
+
Initialize ScanpyUtilMCP with optional tool filtering.
|
235
17
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
return
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
18
|
+
Args:
|
19
|
+
include_tools (list, optional): List of tool names to include. If None, all tools are included.
|
20
|
+
exclude_tools (list, optional): List of tool names to exclude. If None, no tools are excluded.
|
21
|
+
AdataInfo: The AdataInfo class to use for type annotations.
|
22
|
+
"""
|
23
|
+
super().__init__("SCMCP-Util-Server", include_tools, exclude_tools, AdataInfo)
|
24
|
+
|
25
|
+
|
26
|
+
def _tool_query_op_log(self):
|
27
|
+
def _query_op_log(request: QueryOpLogModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
28
|
+
"""Query the adata operation log"""
|
29
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
30
|
+
op_dic = adata.uns["operation"]["op"]
|
31
|
+
opids = adata.uns["operation"]["opid"][-request.n:]
|
32
|
+
op_list = []
|
33
|
+
for opid in opids:
|
34
|
+
op_list.append(op_dic[opid])
|
35
|
+
return op_list
|
36
|
+
return _query_op_log
|
37
|
+
|
38
|
+
def _tool_mark_var(self):
|
39
|
+
def _mark_var(request: MarkVarModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
40
|
+
"""
|
41
|
+
Determine if each gene meets specific conditions and store results in adata.var as boolean values.
|
42
|
+
For example: mitochondrion genes startswith MT-.
|
43
|
+
The tool should be called first when calculate quality control metrics for mitochondrion, ribosomal, harhemoglobin genes, or other qc_vars.
|
44
|
+
"""
|
45
|
+
try:
|
46
|
+
result = forward_request("ul_mark_var", request, adinfo)
|
47
|
+
if result is not None:
|
48
|
+
return result
|
49
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
50
|
+
var_name = request.var_name
|
51
|
+
gene_class = request.gene_class
|
52
|
+
pattern_type = request.pattern_type
|
53
|
+
patterns = request.patterns
|
54
|
+
if gene_class is not None:
|
55
|
+
if gene_class == "mitochondrion":
|
56
|
+
adata.var["mt"] = adata.var_names.str.startswith(('MT-', 'Mt','mt-'))
|
57
|
+
var_name = "mt"
|
58
|
+
elif gene_class == "ribosomal":
|
59
|
+
adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL", "Rps", "Rpl"))
|
60
|
+
var_name = "ribo"
|
61
|
+
elif gene_class == "hemoglobin":
|
62
|
+
adata.var["hb"] = adata.var_names.str.contains("^HB[^(P)]", case=False)
|
63
|
+
var_name = "hb"
|
64
|
+
elif pattern_type is not None and patterns is not None:
|
65
|
+
if pattern_type == "startswith":
|
66
|
+
adata.var[var_name] = adata.var_names.str.startswith(patterns)
|
67
|
+
elif pattern_type == "endswith":
|
68
|
+
adata.var[var_name] = adata.var_names.str.endswith(patterns)
|
69
|
+
elif pattern_type == "contains":
|
70
|
+
adata.var[var_name] = adata.var_names.str.contains(patterns)
|
71
|
+
else:
|
72
|
+
raise ValueError(f"Did not support pattern_type: {pattern_type}")
|
73
|
+
else:
|
74
|
+
raise ValueError(f"Please provide validated parameter")
|
75
|
+
|
76
|
+
res = {var_name: adata.var[var_name].value_counts().to_dict(), "msg": f"add '{var_name}' column in adata.var"}
|
77
|
+
func_kwargs = {"var_name": var_name, "gene_class": gene_class, "pattern_type": pattern_type, "patterns": patterns}
|
78
|
+
add_op_log(adata, "mark_var", func_kwargs, adinfo)
|
79
|
+
return res
|
80
|
+
except ToolError as e:
|
81
|
+
raise ToolError(e)
|
82
|
+
except Exception as e:
|
83
|
+
if hasattr(e, '__context__') and e.__context__:
|
84
|
+
raise ToolError(e.__context__)
|
85
|
+
else:
|
86
|
+
raise ToolError(e)
|
87
|
+
return _mark_var
|
88
|
+
|
89
|
+
def _tool_list_var(self):
|
90
|
+
def _list_var(request: ListVarModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
91
|
+
"""List key columns in adata.var. It should be called for checking when other tools need var key column names as input."""
|
92
|
+
try:
|
93
|
+
result = forward_request("ul_list_var", request, adinfo)
|
94
|
+
if result is not None:
|
95
|
+
return result
|
96
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
97
|
+
columns = list(adata.var.columns)
|
98
|
+
add_op_log(adata, list_var, {}, adinfo)
|
99
|
+
return columns
|
100
|
+
except ToolError as e:
|
101
|
+
raise ToolError(e)
|
102
|
+
except Exception as e:
|
103
|
+
if hasattr(e, '__context__') and e.__context__:
|
104
|
+
raise ToolError(e.__context__)
|
105
|
+
else:
|
106
|
+
raise ToolError(e)
|
107
|
+
return _list_var
|
108
|
+
|
109
|
+
def _tool_list_obs(self):
|
110
|
+
def _list_obs(request: ListObsModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
111
|
+
"""List key columns in adata.obs. It should be called before other tools need obs key column names input."""
|
112
|
+
try:
|
113
|
+
result = forward_request("ul_list_obs", request, adinfo)
|
114
|
+
if result is not None:
|
115
|
+
return result
|
116
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
117
|
+
columns = list(adata.obs.columns)
|
118
|
+
add_op_log(adata, list_obs, {}, adinfo)
|
119
|
+
return columns
|
120
|
+
except ToolError as e:
|
121
|
+
raise ToolError(e)
|
122
|
+
except Exception as e:
|
123
|
+
if hasattr(e, '__context__') and e.__context__:
|
124
|
+
raise ToolError(e.__context__)
|
125
|
+
else:
|
126
|
+
raise ToolError(e)
|
127
|
+
return _list_obs
|
128
|
+
|
129
|
+
def _tool_check_var(self):
|
130
|
+
def _check_var(request: VarNamesModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
131
|
+
"""Check if genes/variables exist in adata.var_names. This tool should be called before gene expression visualizations or color by genes."""
|
132
|
+
try:
|
133
|
+
result = forward_request("ul_check_var", request, adinfo)
|
134
|
+
if result is not None:
|
135
|
+
return result
|
136
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
137
|
+
var_names = request.var_names
|
138
|
+
result = {v: v in adata.var_names for v in var_names}
|
139
|
+
add_op_log(adata, check_var, {"var_names": var_names}, adinfo)
|
140
|
+
return result
|
141
|
+
except ToolError as e:
|
142
|
+
raise ToolError(e)
|
143
|
+
except Exception as e:
|
144
|
+
if hasattr(e, '__context__') and e.__context__:
|
145
|
+
raise ToolError(e.__context__)
|
146
|
+
else:
|
147
|
+
raise ToolError(e)
|
148
|
+
return _check_var
|
149
|
+
|
150
|
+
def _tool_merge_adata(self):
|
151
|
+
def _merge_adata(request: ConcatBaseModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
152
|
+
"""Merge multiple adata objects."""
|
153
|
+
try:
|
154
|
+
result = forward_request("ul_merge_adata", request, adinfo)
|
155
|
+
if result is not None:
|
156
|
+
return result
|
157
|
+
ads = get_ads()
|
158
|
+
adata = ads.get_adata(adinfo=adinfo)
|
159
|
+
kwargs = {k: v for k, v in request.model_dump().items() if v is not None}
|
160
|
+
merged_adata = adata.concat(list(ads.adata_dic[dtype].values()), **kwargs)
|
161
|
+
ads.adata_dic[dtype] = {}
|
162
|
+
ads.active_id = "merged_adata"
|
163
|
+
add_op_log(merged_adata, ad.concat, kwargs, adinfo)
|
164
|
+
ads.adata_dic[ads.active_id] = merged_adata
|
165
|
+
return {"status": "success", "message": "Successfully merged all AnnData objects"}
|
166
|
+
except ToolError as e:
|
167
|
+
raise ToolError(e)
|
168
|
+
except Exception as e:
|
169
|
+
if hasattr(e, '__context__') and e.__context__:
|
170
|
+
raise ToolError(e.__context__)
|
171
|
+
else:
|
172
|
+
raise ToolError(e)
|
173
|
+
return _merge_adata
|
174
|
+
|
175
|
+
def _tool_set_dpt_iroot(self):
|
176
|
+
def _set_dpt_iroot(request: DPTIROOTModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
177
|
+
"""Set the iroot cell"""
|
178
|
+
try:
|
179
|
+
result = forward_request("ul_set_dpt_iroot", request, adinfo)
|
180
|
+
if result is not None:
|
181
|
+
return result
|
182
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
183
|
+
diffmap_key = request.diffmap_key
|
184
|
+
dimension = request.dimension
|
185
|
+
direction = request.direction
|
186
|
+
if diffmap_key not in adata.obsm:
|
187
|
+
raise ValueError(f"Diffusion map key '{diffmap_key}' not found in adata.obsm")
|
188
|
+
if direction == "min":
|
189
|
+
adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmin()
|
190
|
+
else:
|
191
|
+
adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmax()
|
192
|
+
|
193
|
+
func_kwargs = {"diffmap_key": diffmap_key, "dimension": dimension, "direction": direction}
|
194
|
+
add_op_log(adata, "set_dpt_iroot", func_kwargs, adinfo)
|
195
|
+
|
196
|
+
return {"status": "success", "message": f"Successfully set root cell for DPT using {direction} of dimension {dimension}"}
|
197
|
+
except ToolError as e:
|
198
|
+
raise ToolError(e)
|
199
|
+
except Exception as e:
|
200
|
+
if hasattr(e, '__context__') and e.__context__:
|
201
|
+
raise ToolError(e.__context__)
|
202
|
+
else:
|
203
|
+
raise ToolError(e)
|
204
|
+
return _set_dpt_iroot
|
205
|
+
|
206
|
+
def _tool_add_layer(self):
|
207
|
+
def _add_layer(request: AddLayerModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
208
|
+
"""Add a layer to the AnnData object."""
|
209
|
+
try:
|
210
|
+
result = forward_request("ul_add_layer", request, adinfo)
|
211
|
+
if result is not None:
|
212
|
+
return result
|
213
|
+
adata = get_ads().get_adata(adinfo=adinfo)
|
214
|
+
layer_name = request.layer_name
|
215
|
+
|
216
|
+
# Check if layer already exists
|
217
|
+
if layer_name in adata.layers:
|
218
|
+
raise ValueError(f"Layer '{layer_name}' already exists in adata.layers")
|
219
|
+
# Add the data as a new layer
|
220
|
+
adata.layers[layer_name] = adata.X.copy()
|
221
|
+
|
222
|
+
func_kwargs = {"layer_name": layer_name}
|
223
|
+
add_op_log(adata, "add_layer", func_kwargs, adinfo)
|
224
|
+
|
225
|
+
return {
|
226
|
+
"status": "success",
|
227
|
+
"message": f"Successfully added layer '{layer_name}' to adata.layers"
|
228
|
+
}
|
229
|
+
except ToolError as e:
|
230
|
+
raise ToolError(e)
|
231
|
+
except Exception as e:
|
232
|
+
if hasattr(e, '__context__') and e.__context__:
|
233
|
+
raise ToolError(e.__context__)
|
234
|
+
else:
|
235
|
+
raise ToolError(e)
|
236
|
+
return _add_layer
|
237
|
+
|
238
|
+
def _tool_check_samples(self):
|
239
|
+
def _check_samples(request: None, adinfo: self.AdataInfo=self.AdataInfo()):
|
240
|
+
"""check the stored samples"""
|
241
|
+
try:
|
242
|
+
ads = get_ads()
|
243
|
+
return {"sampleid": [list(ads.adata_dic[dk].keys()) for dk in ads.adata_dic.keys()]}
|
244
|
+
except ToolError as e:
|
245
|
+
raise ToolError(e)
|
246
|
+
except Exception as e:
|
247
|
+
if hasattr(e, '__context__') and e.__context__:
|
248
|
+
raise ToolError(e.__context__)
|
249
|
+
else:
|
250
|
+
raise ToolError(e)
|
251
|
+
return _check_samples
|
scmcp_shared/util.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
import inspect
|
2
2
|
import os
|
3
|
+
from enum import Enum
|
3
4
|
from pathlib import Path
|
4
5
|
from fastmcp.server.dependencies import get_context
|
5
6
|
from fastmcp.exceptions import ToolError
|
7
|
+
import asyncio
|
8
|
+
import nest_asyncio
|
6
9
|
|
7
10
|
|
8
11
|
def get_env(key):
|
@@ -58,7 +61,7 @@ def add_op_log(adata, func, kwargs, adinfo):
|
|
58
61
|
|
59
62
|
|
60
63
|
|
61
|
-
def
|
64
|
+
def save_fig_path(axes, file):
|
62
65
|
from matplotlib.axes import Axes
|
63
66
|
|
64
67
|
try:
|
@@ -84,7 +87,7 @@ def savefig(axes, file):
|
|
84
87
|
raise e
|
85
88
|
|
86
89
|
|
87
|
-
def
|
90
|
+
def savefig(axes, func=None, **kwargs):
|
88
91
|
if hasattr(func, "func") and hasattr(func.func, "__name__"):
|
89
92
|
# For partial functions, use the original function name
|
90
93
|
func_name = func.func.__name__
|
@@ -107,9 +110,9 @@ def set_fig_path(axes, func=None, **kwargs):
|
|
107
110
|
args_str = "_".join(args).replace(" ", "")
|
108
111
|
fig_path = fig_dir / f"{func_name}_{args_str}.png"
|
109
112
|
try:
|
110
|
-
|
113
|
+
save_fig_path(axes, fig_path)
|
111
114
|
except PermissionError:
|
112
|
-
raise PermissionError("You don't have permission to
|
115
|
+
raise PermissionError("You don't have permission to save figure")
|
113
116
|
except Exception as e:
|
114
117
|
raise e
|
115
118
|
transport = get_env("TRANSPORT")
|
@@ -140,7 +143,7 @@ def add_figure_route(server):
|
|
140
143
|
server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
|
141
144
|
|
142
145
|
|
143
|
-
async def
|
146
|
+
async def async_forward_request(func, request, adinfo, **kwargs):
|
144
147
|
from fastmcp import Client
|
145
148
|
forward_url = get_env("FORWARD")
|
146
149
|
request_kwargs = request.model_dump()
|
@@ -149,8 +152,6 @@ async def forward_request(func, request, adinfo, **kwargs):
|
|
149
152
|
"request": {k: request_kwargs.get(k) for k in request_args},
|
150
153
|
"adinfo": adinfo.model_dump()
|
151
154
|
}
|
152
|
-
print(func_kwargs)
|
153
|
-
# func_kwargs.update({k:v for k,v in kwargs.items() if v is not None})
|
154
155
|
if not forward_url:
|
155
156
|
return None
|
156
157
|
|
@@ -170,13 +171,35 @@ async def forward_request(func, request, adinfo, **kwargs):
|
|
170
171
|
raise e
|
171
172
|
|
172
173
|
|
174
|
+
|
175
|
+
def forward_request(func, request, adinfo, **kwargs):
|
176
|
+
"""Synchronous wrapper for forward_request"""
|
177
|
+
try:
|
178
|
+
# Apply nest_asyncio to allow nested event loops
|
179
|
+
nest_asyncio.apply()
|
180
|
+
loop = asyncio.get_event_loop()
|
181
|
+
if loop.is_running():
|
182
|
+
# If we're in a running event loop, use create_task
|
183
|
+
async def _run():
|
184
|
+
return await async_forward_request(func, request, adinfo, **kwargs)
|
185
|
+
return loop.run_until_complete(_run())
|
186
|
+
else:
|
187
|
+
# If no event loop is running, use asyncio.run()
|
188
|
+
return asyncio.run(async_forward_request(func, request, adinfo, **kwargs))
|
189
|
+
except Exception as e:
|
190
|
+
if hasattr(e, '__context__') and e.__context__:
|
191
|
+
raise Exception(f"{str(e.__context__)}")
|
192
|
+
else:
|
193
|
+
raise e
|
194
|
+
|
195
|
+
|
173
196
|
def obsm2adata(adata, obsm_key):
|
174
197
|
from anndata import AnnData
|
175
198
|
|
176
199
|
if obsm_key not in adata.obsm_keys():
|
177
200
|
raise ValueError(f"key {obsm_key} not found in adata.obsm")
|
178
201
|
else:
|
179
|
-
return AnnData(adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm)
|
202
|
+
return AnnData(adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm, uns=adata.uns)
|
180
203
|
|
181
204
|
|
182
205
|
def get_ads():
|
@@ -196,16 +219,58 @@ def sc_like_plot(plot_func, adata, request, adinfo, **kwargs):
|
|
196
219
|
axes = plot_func(adata, **func_kwargs)
|
197
220
|
if axes is None:
|
198
221
|
axes = plt.gca()
|
199
|
-
fig_path =
|
222
|
+
fig_path = savefig(axes, plot_func, **func_kwargs)
|
200
223
|
add_op_log(adata, plot_func, func_kwargs, adinfo)
|
201
224
|
return fig_path
|
202
225
|
|
203
226
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
227
|
+
def filter_tools(mcp, include_tools=None, exclude_tools=None):
|
228
|
+
import asyncio
|
229
|
+
import copy
|
230
|
+
mcp = copy.deepcopy(mcp)
|
231
|
+
async def _filter_tools(mcp, include_tools=None, exclude_tools=None):
|
232
|
+
tools = await mcp.get_tools()
|
233
|
+
for tool in tools:
|
234
|
+
if exclude_tools and tool in exclude_tools:
|
235
|
+
mcp.remove_tool(tool)
|
236
|
+
if include_tools and tool not in include_tools:
|
237
|
+
mcp.remove_tool(tool)
|
238
|
+
return mcp
|
239
|
+
return asyncio.run(_filter_tools(mcp, include_tools, exclude_tools))
|
240
|
+
|
241
|
+
|
242
|
+
|
243
|
+
def set_env(log_file, forward, transport, host, port):
|
244
|
+
if log_file is not None:
|
245
|
+
os.environ['SCMCP_LOG_FILE'] = log_file
|
246
|
+
if forward is not None:
|
247
|
+
os.environ['SCMCP_FORWARD'] = forward
|
248
|
+
os.environ['SCMCP_TRANSPORT'] = transport
|
249
|
+
os.environ['SCMCP_HOST'] = host
|
250
|
+
os.environ['SCMCP_PORT'] = str(port)
|
251
|
+
|
252
|
+
|
253
|
+
|
254
|
+
def setup_mcp(mcp, sub_mcp_dic, modules=None):
|
255
|
+
import asyncio
|
256
|
+
if modules is None or modules == "all":
|
257
|
+
modules = sub_mcp_dic.keys()
|
258
|
+
for module in modules:
|
259
|
+
asyncio.run(mcp.import_server(module, sub_mcp_dic[module]))
|
260
|
+
return mcp
|
261
|
+
|
262
|
+
def _update_args(mcp, func, args_dic : dict):
|
263
|
+
defs = mcp._tool_manager._tools[func].parameters['$defs']
|
264
|
+
model_names = list(defs.keys())
|
265
|
+
args_model = model_names[0] if model_names[0] != "AdataModel" else model_names[1]
|
266
|
+
for args, property_dic in args_dic.items():
|
267
|
+
for pk, v in property_dic.items():
|
268
|
+
for model in model_names:
|
269
|
+
if args in mcp._tool_manager._tools[func].parameters['$defs'][model]["properties"]:
|
270
|
+
mcp._tool_manager._tools[func].parameters['$defs'][model]["properties"][args][pk] = v
|
271
|
+
|
272
|
+
|
273
|
+
def update_mcp_args(mcp, tool_args : dict):
|
274
|
+
tools = mcp._tool_manager._tools.keys()
|
275
|
+
for tool in tool_args:
|
276
|
+
_update_args(mcp, tool, tool_args[tool])
|