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.
Files changed (24) hide show
  1. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/PKG-INFO +5 -1
  2. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/pyproject.toml +18 -0
  3. scmcp_shared-0.3.6/src/scmcp_shared/__init__.py +3 -0
  4. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/io.py +3 -3
  5. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/io.py +12 -2
  6. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/pp.py +5 -5
  7. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/tl.py +4 -4
  8. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/util.py +10 -6
  9. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/util.py +11 -1
  10. scmcp_shared-0.3.5/src/scmcp_shared/__init__.py +0 -3
  11. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/.github/release.yml +0 -0
  12. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/.github/workflows/publish.yml +0 -0
  13. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/LICENSE +0 -0
  14. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/README.md +0 -0
  15. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/cli.py +0 -0
  16. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/logging_config.py +0 -0
  17. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/__init__.py +0 -0
  18. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/pl.py +0 -0
  19. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/pp.py +0 -0
  20. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/tl.py +0 -0
  21. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/schema/util.py +0 -0
  22. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/__init__.py +0 -0
  23. {scmcp_shared-0.3.5 → scmcp_shared-0.3.6}/src/scmcp_shared/server/base.py +0 -0
  24. {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.5
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"
@@ -0,0 +1,3 @@
1
+
2
+ __version__ = "0.3.6"
3
+
@@ -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
- first_column_obs: bool = Field(
34
- default=True,
35
- description="If True, assume the first column stores observations (cell or barcode) names when provide text file. If False, the data will be transposed."
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("first_column_obs", True):
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 generate_msg(adinfo, adata, ads)
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 is not None:
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, "var_value": request.var_value,
94
- "var_min": request.var_min, "var_max": request.var_max, "hpv": request.highly_variable
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
- result = {v: v in adata.var_names for v in var_names}
139
- add_op_log(adata, check_var, {"var_names": var_names}, adinfo)
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(list(ads.adata_dic[dtype].values()), **kwargs)
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, "dtype": adinfo.adtype, "adata": adata}
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']}")
@@ -1,3 +0,0 @@
1
-
2
- __version__ = "0.3.5"
3
-
File without changes
File without changes