scmcp-shared 0.3.5__tar.gz → 0.3.6__tar.gz
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-0.3.5 → scmcp_shared-0.3.6}/PKG-INFO +5 -1
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/pyproject.toml +18 -0
- scmcp_shared-0.3.6/src/scmcp_shared/__init__.py +3 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/io.py +3 -3
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/io.py +12 -2
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/pp.py +5 -5
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/tl.py +4 -4
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/util.py +10 -6
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/util.py +11 -1
- scmcp_shared-0.3.5/src/scmcp_shared/__init__.py +0 -3
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/.github/release.yml +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/.github/workflows/publish.yml +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/LICENSE +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/README.md +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/cli.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/logging_config.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/__init__.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/pl.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/pp.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/tl.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/util.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/__init__.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/base.py +0 -0
- {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/pl.py +0 -0
@@ -1,7 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: scmcp_shared
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.6
|
4
4
|
Summary: A shared function libray for scmcphub
|
5
|
+
Project-URL: Homepage, http://scmcphub.org/
|
6
|
+
Project-URL: Repository, https://github.com/scmcphub/scmcp-shared
|
7
|
+
Project-URL: Documentation, https://docs.scmcphub.org/
|
5
8
|
Author-email: shuang <hsh-me@outlook.com>
|
6
9
|
License: BSD 3-Clause License
|
7
10
|
|
@@ -32,6 +35,7 @@ License: BSD 3-Clause License
|
|
32
35
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
33
36
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
37
|
License-File: LICENSE
|
38
|
+
Keywords: AI,agent,bioinformatics,llm,mcp,model context protocol,scRNA-seq,single cell
|
35
39
|
Requires-Python: >=3.10
|
36
40
|
Requires-Dist: fastmcp>=2.3.4
|
37
41
|
Requires-Dist: mcp>=1.8.0
|
@@ -7,6 +7,18 @@ license = { file = "LICENSE" }
|
|
7
7
|
authors = [
|
8
8
|
{ name = "shuang", email = "hsh-me@outlook.com" }
|
9
9
|
]
|
10
|
+
|
11
|
+
keywords = [
|
12
|
+
"mcp",
|
13
|
+
"model context protocol",
|
14
|
+
"llm",
|
15
|
+
"agent",
|
16
|
+
"scRNA-seq",
|
17
|
+
"single cell",
|
18
|
+
"bioinformatics",
|
19
|
+
"AI"
|
20
|
+
]
|
21
|
+
|
10
22
|
requires-python = ">=3.10"
|
11
23
|
dependencies = [
|
12
24
|
"scanpy",
|
@@ -28,6 +40,12 @@ dev = [
|
|
28
40
|
[tool.hatch.version]
|
29
41
|
path = "src/scmcp_shared/__init__.py"
|
30
42
|
|
43
|
+
[project.urls]
|
44
|
+
Homepage = "http://scmcphub.org/"
|
45
|
+
Repository = "https://github.com/scmcphub/scmcp-shared"
|
46
|
+
Documentation = "https://docs.scmcphub.org/"
|
47
|
+
|
48
|
+
|
31
49
|
[tool.pytest.ini_options]
|
32
50
|
asyncio_mode = "strict"
|
33
51
|
asyncio_default_fixture_loop_scope = "function"
|
@@ -30,9 +30,9 @@ class ReadModel(BaseModel):
|
|
30
30
|
default=False,
|
31
31
|
description="Assume the first column stores row names. This is only necessary if these are not strings: strings in the first column are automatically assumed to be row names."
|
32
32
|
)
|
33
|
-
|
34
|
-
default=
|
35
|
-
description="If True,
|
33
|
+
transpose: bool = Field(
|
34
|
+
default=False,
|
35
|
+
description="If True, the data will be transposed."
|
36
36
|
)
|
37
37
|
backup_url: str = Field(
|
38
38
|
default=None,
|
@@ -33,7 +33,7 @@ class ScanpyIOMCP(BaseMCP):
|
|
33
33
|
elif file.is_file():
|
34
34
|
func_kwargs = filter_args(request, sc.read)
|
35
35
|
adata = sc.read(**func_kwargs)
|
36
|
-
if not kwargs.get("
|
36
|
+
if not kwargs.get("transpose", True):
|
37
37
|
adata = adata.T
|
38
38
|
else:
|
39
39
|
raise FileNotFoundError(f"{kwargs['filename']} does not exist")
|
@@ -47,8 +47,18 @@ class ScanpyIOMCP(BaseMCP):
|
|
47
47
|
adata.layers["counts"] = adata.X
|
48
48
|
adata.var_names_make_unique()
|
49
49
|
adata.obs_names_make_unique()
|
50
|
+
adata.obs["scmcp_sampleid"] = adinfo.sampleid or ads.active_id
|
50
51
|
ads.set_adata(adata, adinfo=adinfo)
|
51
|
-
return
|
52
|
+
return [
|
53
|
+
{
|
54
|
+
"sampleid": adinfo.sampleid or ads.active_id,
|
55
|
+
"adtype": adinfo.adtype,
|
56
|
+
"adata": adata,
|
57
|
+
"adata.obs_names[:10]": adata.obs_names[:10],
|
58
|
+
"adata.var_names[:10]": adata.var_names[:10],
|
59
|
+
"notice": "check obs_names and var_names. transpose the data if needed"
|
60
|
+
}
|
61
|
+
]
|
52
62
|
except ToolError as e:
|
53
63
|
raise ToolError(e)
|
54
64
|
except Exception as e:
|
@@ -86,12 +86,12 @@ class ScanpyPreprocessingMCP(BaseMCP):
|
|
86
86
|
if request.var_max is not None:
|
87
87
|
mask = mask & (adata.var[request.var_key] <= request.var_max)
|
88
88
|
adata = adata[:, mask]
|
89
|
-
if request.highly_variable
|
90
|
-
adata = adata[:, adata.var.highly_variable]
|
89
|
+
if request.highly_variable:
|
90
|
+
adata = adata[:, mask & adata.var.highly_variable]
|
91
91
|
add_op_log(adata, "subset_genes",
|
92
92
|
{
|
93
|
-
"var_key": request.var_key, "
|
94
|
-
"
|
93
|
+
"var_key": request.var_key, "var_min": request.var_min, "var_max": request.var_max,
|
94
|
+
"hpv": request.highly_variable
|
95
95
|
}, adinfo
|
96
96
|
)
|
97
97
|
ads.set_adata(adata, adinfo=adinfo)
|
@@ -133,7 +133,7 @@ class ScanpyPreprocessingMCP(BaseMCP):
|
|
133
133
|
return _calculate_qc_metrics
|
134
134
|
|
135
135
|
def _tool_log1p(self):
|
136
|
-
def _log1p(request: Log1PModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
136
|
+
def _log1p(request: Log1PModel=Log1PModel(), adinfo: self.AdataInfo=self.AdataInfo()):
|
137
137
|
"""Logarithmize the data matrix"""
|
138
138
|
try:
|
139
139
|
result = forward_request("pp_log1p", request, adinfo)
|
@@ -23,7 +23,7 @@ class ScanpyToolsMCP(BaseMCP):
|
|
23
23
|
super().__init__("ScanpyMCP-TL-Server", include_tools, exclude_tools, AdataInfo)
|
24
24
|
|
25
25
|
def _tool_tsne(self):
|
26
|
-
def _tsne(request: TSNEModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
26
|
+
def _tsne(request: TSNEModel=TSNEModel(), adinfo: self.AdataInfo=self.AdataInfo()):
|
27
27
|
"""t-distributed stochastic neighborhood embedding (t-SNE) for visualization"""
|
28
28
|
try:
|
29
29
|
result = forward_request("tl_tsne", request, adinfo)
|
@@ -45,7 +45,7 @@ class ScanpyToolsMCP(BaseMCP):
|
|
45
45
|
return _tsne
|
46
46
|
|
47
47
|
def _tool_umap(self):
|
48
|
-
def _umap(request: UMAPModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
48
|
+
def _umap(request: UMAPModel=UMAPModel(), adinfo: self.AdataInfo=self.AdataInfo()):
|
49
49
|
"""Uniform Manifold Approximation and Projection (UMAP) for visualization"""
|
50
50
|
try:
|
51
51
|
result = forward_request("tl_umap", request, adinfo)
|
@@ -134,7 +134,7 @@ class ScanpyToolsMCP(BaseMCP):
|
|
134
134
|
return _embedding_density
|
135
135
|
|
136
136
|
def _tool_leiden(self):
|
137
|
-
def _leiden(request: LeidenModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
137
|
+
def _leiden(request: LeidenModel=LeidenModel(), adinfo: self.AdataInfo=self.AdataInfo()):
|
138
138
|
"""Leiden clustering algorithm for community detection"""
|
139
139
|
try:
|
140
140
|
result = forward_request("tl_leiden", request, adinfo)
|
@@ -156,7 +156,7 @@ class ScanpyToolsMCP(BaseMCP):
|
|
156
156
|
return _leiden
|
157
157
|
|
158
158
|
def _tool_louvain(self):
|
159
|
-
def _louvain(request: LouvainModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
159
|
+
def _louvain(request: LouvainModel=LouvainModel(), adinfo: self.AdataInfo=self.AdataInfo()):
|
160
160
|
"""Louvain clustering algorithm for community detection"""
|
161
161
|
try:
|
162
162
|
result = forward_request("tl_louvain", request, adinfo)
|
@@ -87,7 +87,7 @@ class ScanpyUtilMCP(BaseMCP):
|
|
87
87
|
return _mark_var
|
88
88
|
|
89
89
|
def _tool_list_var(self):
|
90
|
-
def _list_var(request: ListVarModel, adinfo: self.AdataInfo=self.AdataInfo()):
|
90
|
+
def _list_var(request: ListVarModel=ListVarModel(), adinfo: self.AdataInfo=self.AdataInfo()):
|
91
91
|
"""List key columns in adata.var. It should be called for checking when other tools need var key column names as input."""
|
92
92
|
try:
|
93
93
|
result = forward_request("ul_list_var", request, adinfo)
|
@@ -95,7 +95,7 @@ class ScanpyUtilMCP(BaseMCP):
|
|
95
95
|
return result
|
96
96
|
adata = get_ads().get_adata(adinfo=adinfo)
|
97
97
|
columns = list(adata.var.columns)
|
98
|
-
add_op_log(adata, list_var, {}, adinfo)
|
98
|
+
add_op_log(adata, "list_var", {}, adinfo)
|
99
99
|
return columns
|
100
100
|
except ToolError as e:
|
101
101
|
raise ToolError(e)
|
@@ -115,7 +115,7 @@ class ScanpyUtilMCP(BaseMCP):
|
|
115
115
|
return result
|
116
116
|
adata = get_ads().get_adata(adinfo=adinfo)
|
117
117
|
columns = list(adata.obs.columns)
|
118
|
-
add_op_log(adata, list_obs, {}, adinfo)
|
118
|
+
add_op_log(adata, "list_obs", {}, adinfo)
|
119
119
|
return columns
|
120
120
|
except ToolError as e:
|
121
121
|
raise ToolError(e)
|
@@ -135,8 +135,12 @@ class ScanpyUtilMCP(BaseMCP):
|
|
135
135
|
return result
|
136
136
|
adata = get_ads().get_adata(adinfo=adinfo)
|
137
137
|
var_names = request.var_names
|
138
|
-
|
139
|
-
|
138
|
+
if adata.raw is not None:
|
139
|
+
all_var_names = adata.raw.to_adata().var_names
|
140
|
+
else:
|
141
|
+
all_var_names = adata.var_names
|
142
|
+
result = {v: v in all_var_names for v in var_names}
|
143
|
+
add_op_log(adata, "check_var", {"var_names": var_names}, adinfo)
|
140
144
|
return result
|
141
145
|
except ToolError as e:
|
142
146
|
raise ToolError(e)
|
@@ -157,7 +161,7 @@ class ScanpyUtilMCP(BaseMCP):
|
|
157
161
|
ads = get_ads()
|
158
162
|
adata = ads.get_adata(adinfo=adinfo)
|
159
163
|
kwargs = {k: v for k, v in request.model_dump().items() if v is not None}
|
160
|
-
merged_adata = adata.concat(
|
164
|
+
merged_adata = adata.concat(ads.adata_dic, **kwargs)
|
161
165
|
ads.adata_dic[dtype] = {}
|
162
166
|
ads.active_id = "merged_adata"
|
163
167
|
add_op_log(merged_adata, ad.concat, kwargs, adinfo)
|
@@ -104,6 +104,10 @@ def savefig(axes, func=None, **kwargs):
|
|
104
104
|
args = []
|
105
105
|
for k,v in kwargs.items():
|
106
106
|
if isinstance(v, (tuple, list, set)):
|
107
|
+
v = v[:3] ## show first 3 elements
|
108
|
+
args.append(f"{k}-{'-'.join([str(i) for i in v])}")
|
109
|
+
elif isinstance(v, dict):
|
110
|
+
v = list(v.keys())[:3] ## show first 3 elements
|
107
111
|
args.append(f"{k}-{'-'.join([str(i) for i in v])}")
|
108
112
|
else:
|
109
113
|
args.append(f"{k}-{v}")
|
@@ -209,7 +213,7 @@ def get_ads():
|
|
209
213
|
|
210
214
|
|
211
215
|
def generate_msg(adinfo, adata, ads):
|
212
|
-
return {"sampleid": adinfo.sampleid or ads.active_id, "
|
216
|
+
return {"sampleid": adinfo.sampleid or ads.active_id, "adtype": adinfo.adtype, "adata": adata}
|
213
217
|
|
214
218
|
|
215
219
|
def sc_like_plot(plot_func, adata, request, adinfo, **kwargs):
|
@@ -270,3 +274,9 @@ def update_mcp_args(mcp, tool_args : dict):
|
|
270
274
|
tools = mcp._tool_manager._tools.keys()
|
271
275
|
for tool in tool_args:
|
272
276
|
_update_args(mcp, tool, tool_args[tool])
|
277
|
+
|
278
|
+
|
279
|
+
def check_adata(adata, adinfo, ads):
|
280
|
+
sampleid = adinfo.sampleid or ads.active_id
|
281
|
+
if sampleid != adata.uns["scmcp_sampleid"]:
|
282
|
+
raise ValueError(f"sampleid mismatch: {sampleid} != {adata.uns['scmcp_sampleid']}")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|