scmcp-shared 0.2.0__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.
@@ -3,248 +3,249 @@ import inspect
3
3
  from pathlib import Path
4
4
  import scanpy as sc
5
5
  from fastmcp import FastMCP , Context
6
+ from fastmcp.exceptions import ToolError
6
7
  from ..schema.util import *
8
+ from ..schema import AdataModel, AdataInfo
7
9
  from ..util import filter_args, forward_request, get_ads, generate_msg,add_op_log
10
+ from .base import BaseMCP
8
11
 
9
12
 
10
- ul_mcp = FastMCP("SCMCP-Util-Server")
11
-
12
-
13
- @ul_mcp.tool()
14
- async def query_op_log(request: QueryOpLogModel = QueryOpLogModel()):
15
- """Query the adata operation log"""
16
- adata = get_ads().get_adata(request=request)
17
- op_dic = adata.uns["operation"]["op"]
18
- opids = adata.uns["operation"]["opid"][-n:]
19
- op_list = []
20
- for opid in opids:
21
- op_list.append(op_dic[opid])
22
- return op_list
23
-
24
-
25
- @ul_mcp.tool()
26
- async def mark_var(
27
- request: MarkVarModel = MarkVarModel()
28
- ):
29
- """
30
- Determine if each gene meets specific conditions and store results in adata.var as boolean values.
31
- For example: mitochondrion genes startswith MT-.
32
- The tool should be called first when calculate quality control metrics for mitochondrion, ribosomal, harhemoglobin genes, or other qc_vars.
33
- """
34
- try:
35
- result = await forward_request("ul_mark_var", request)
36
- if result is not None:
37
- return result
38
- adata = get_ads().get_adata(request=request)
39
- var_name = request.var_name
40
- gene_class = request.gene_class
41
- pattern_type = request.pattern_type
42
- patterns = request.patterns
43
- if gene_class is not None:
44
- if gene_class == "mitochondrion":
45
- adata.var["mt"] = adata.var_names.str.startswith(('MT-', 'Mt','mt-'))
46
- var_name = "mt"
47
- elif gene_class == "ribosomal":
48
- adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL", "Rps", "Rpl"))
49
- var_name = "ribo"
50
- elif gene_class == "hemoglobin":
51
- adata.var["hb"] = adata.var_names.str.contains("^HB[^(P)]", case=False)
52
- var_name = "hb"
53
- elif pattern_type is not None and patterns is not None:
54
- if pattern_type == "startswith":
55
- adata.var[var_name] = adata.var_names.str.startswith(patterns)
56
- elif pattern_type == "endswith":
57
- adata.var[var_name] = adata.var_names.str.endswith(patterns)
58
- elif pattern_type == "contains":
59
- adata.var[var_name] = adata.var_names.str.contains(patterns)
60
- else:
61
- raise ValueError(f"Did not support pattern_type: {pattern_type}")
62
- else:
63
- raise ValueError(f"Please provide validated parameter")
64
-
65
- res = {var_name: adata.var[var_name].value_counts().to_dict(), "msg": f"add '{var_name}' column in adata.var"}
66
- func_kwargs = {"var_name": var_name, "gene_class": gene_class, "pattern_type": pattern_type, "patterns": patterns}
67
- add_op_log(adata, "mark_var", func_kwargs)
68
- return res
69
- except KeyError as e:
70
- raise e
71
- except Exception as e:
72
- if hasattr(e, '__context__') and e.__context__:
73
- raise Exception(f"{str(e.__context__)}")
74
- else:
75
- raise e
76
-
77
-
78
- @ul_mcp.tool()
79
- async def list_var(
80
- request: ListVarModel = ListVarModel()
81
- ):
82
- """List key columns in adata.var. It should be called for checking when other tools need var key column names as input."""
83
- try:
84
- result = await forward_request("ul_list_var", request)
85
- if result is not None:
86
- return result
87
- adata = get_ads().get_adata(request=request)
88
- columns = list(adata.var.columns)
89
- add_op_log(adata, list_var, {})
90
- return columns
91
- except KeyError as e:
92
- raise e
93
- except Exception as e:
94
- if hasattr(e, '__context__') and e.__context__:
95
- raise Exception(f"{str(e.__context__)}")
96
- else:
97
- raise e
98
-
99
- @ul_mcp.tool()
100
- async def list_obs(
101
- request: ListObsModel = ListObsModel()
102
- ):
103
- """List key columns in adata.obs. It should be called before other tools need obs key column names input."""
104
- try:
105
- result = await forward_request("ul_list_obs", request)
106
- if result is not None:
107
- return result
108
- adata = get_ads().get_adata(request=request)
109
- columns = list(adata.obs.columns)
110
- add_op_log(adata, list_obs, {})
111
- return columns
112
- except KeyError as e:
113
- raise e
114
- except Exception as e:
115
- if hasattr(e, '__context__') and e.__context__:
116
- raise Exception(f"{str(e.__context__)}")
117
- else:
118
- raise e
119
-
120
- @ul_mcp.tool()
121
- async def check_var(
122
- request: VarNamesModel = VarNamesModel()
123
- ):
124
- """Check if genes/variables exist in adata.var_names. This tool should be called before gene expression visualizations or color by genes."""
125
- try:
126
- result = await forward_request("ul_check_var", request)
127
- if result is not None:
128
- return result
129
- adata = get_ads().get_adata(request=request)
130
- var_names = request.var_names
131
- result = {v: v in adata.var_names for v in var_names}
132
- add_op_log(adata, check_var, {"var_names": var_names})
133
- return result
134
- except KeyError as e:
135
- raise e
136
- except Exception as e:
137
- if hasattr(e, '__context__') and e.__context__:
138
- raise Exception(f"{str(e.__context__)}")
139
- else:
140
- raise e
141
-
142
- @ul_mcp.tool()
143
- async def merge_adata(
144
- request: ConcatAdataModel = ConcatAdataModel()
145
- ):
146
- """Merge multiple adata objects."""
147
-
148
- try:
149
- result = await forward_request("ul_merge_adata", request)
150
- if result is not None:
151
- return result
152
- ads = get_ads()
153
- adata = ads.get_adata(request=request)
154
- kwargs = {k: v for k, v in request.model_dump().items() if v is not None}
155
- merged_adata = adata.concat(list(ads.adata_dic[dtype].values()), **kwargs)
156
- ads.adata_dic[dtype] = {}
157
- ads.active_id = "merged_adata"
158
- add_op_log(merged_adata, ad.concat, kwargs)
159
- ads.adata_dic[ads.active_id] = merged_adata
160
- return {"status": "success", "message": "Successfully merged all AnnData objects"}
161
- except KeyError as e:
162
- raise e
163
- except Exception as e:
164
- if hasattr(e, '__context__') and e.__context__:
165
- raise Exception(f"{str(e.__context__)}")
166
- else:
167
- raise e
168
-
169
-
170
- @ul_mcp.tool()
171
- async def set_dpt_iroot(
172
- request: DPTIROOTModel,
173
-
174
- ):
175
- """Set the iroot cell"""
176
- try:
177
- result = await forward_request("ul_set_dpt_iroot", request)
178
- if result is not None:
179
- return result
180
- adata = get_ads().get_adata(request=request)
181
- diffmap_key = request.diffmap_key
182
- dimension = request.dimension
183
- direction = request.direction
184
- if diffmap_key not in adata.obsm:
185
- raise ValueError(f"Diffusion map key '{diffmap_key}' not found in adata.obsm")
186
- if direction == "min":
187
- adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmin()
188
- else:
189
- adata.uns["iroot"] = adata.obsm[diffmap_key][:, dimension].argmax()
190
-
191
- func_kwargs = {"diffmap_key": diffmap_key, "dimension": dimension, "direction": direction}
192
- add_op_log(adata, "set_dpt_iroot", func_kwargs)
193
-
194
- return {"status": "success", "message": f"Successfully set root cell for DPT using {direction} of dimension {dimension}"}
195
- except KeyError as e:
196
- raise e
197
- except Exception as e:
198
- if hasattr(e, '__context__') and e.__context__:
199
- raise Exception(f"{str(e.__context__)}")
200
- else:
201
- raise e
202
-
203
- @ul_mcp.tool()
204
- async def add_layer(
205
- request: AddLayerModel,
206
- ):
207
- """Add a layer to the AnnData object.
208
- """
209
- try:
210
- result = await forward_request("ul_add_layer", request)
211
- if result is not None:
212
- return result
213
- adata = get_ads().get_adata(request=request)
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)
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.
224
17
 
225
- return {
226
- "status": "success",
227
- "message": f"Successfully added layer '{layer_name}' to adata.layers"
228
- }
229
- except KeyError as e:
230
- raise e
231
- except Exception as e:
232
- if hasattr(e, '__context__') and e.__context__:
233
- raise Exception(f"{str(e.__context__)}")
234
- else:
235
- raise e
236
-
237
- @ul_mcp.tool()
238
- async def check_samples():
239
- """check the stored samples
240
- """
241
- try:
242
- ads = get_ads()
243
- return {"sampleid": [list(ads.adata_dic[dk].keys()) for dk in ads.adata_dic.keys()]}
244
- except KeyError as e:
245
- raise e
246
- except Exception as e:
247
- if hasattr(e, '__context__') and e.__context__:
248
- raise Exception(f"{str(e.__context__)}")
249
- else:
250
- raise e
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