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
scmcp_shared/schema/util.py
CHANGED
@@ -1,99 +1,93 @@
|
|
1
|
+
from pydantic import Field, BaseModel
|
2
|
+
from typing import Optional, List, Dict, Any, Literal
|
1
3
|
|
2
|
-
from pydantic import (
|
3
|
-
Field,
|
4
|
-
ValidationInfo,
|
5
|
-
computed_field,
|
6
|
-
field_validator,
|
7
|
-
model_validator,BaseModel
|
8
|
-
)
|
9
|
-
from typing import Optional, Union, List, Dict, Any, Callable, Collection, Literal
|
10
4
|
|
11
|
-
|
12
|
-
|
13
|
-
class MarkVarParams(BaseModel):
|
5
|
+
class MarkVarParam(BaseModel):
|
14
6
|
"""Determine or mark if each gene meets specific conditions and store results in adata.var as boolean values"""
|
15
|
-
|
7
|
+
|
16
8
|
var_name: str = Field(
|
17
9
|
default=None,
|
18
|
-
description="Column name that will be added to adata.var, do not set if user does not ask"
|
10
|
+
description="Column name that will be added to adata.var, do not set if user does not ask",
|
19
11
|
)
|
20
12
|
pattern_type: Optional[Literal["startswith", "endswith", "contains"]] = Field(
|
21
13
|
default=None,
|
22
|
-
description="Pattern matching type (startswith/endswith/contains), it should be None when gene_class is not None"
|
23
|
-
)
|
14
|
+
description="Pattern matching type (startswith/endswith/contains), it should be None when gene_class is not None",
|
15
|
+
)
|
24
16
|
patterns: Optional[str] = Field(
|
25
17
|
default=None,
|
26
|
-
description="gene pattern to match, must be a string, it should be None when gene_class is not None"
|
18
|
+
description="gene pattern to match, must be a string, it should be None when gene_class is not None",
|
27
19
|
)
|
28
|
-
|
20
|
+
|
29
21
|
gene_class: Optional[Literal["mitochondrion", "ribosomal", "hemoglobin"]] = Field(
|
30
|
-
default=None,
|
31
|
-
description="Gene class type (Mitochondrion/Ribosomal/Hemoglobin)"
|
22
|
+
default=None, description="Gene class type (Mitochondrion/Ribosomal/Hemoglobin)"
|
32
23
|
)
|
33
24
|
|
34
25
|
|
35
|
-
class
|
36
|
-
"""ListVarModel"""
|
26
|
+
class ListVarParam(BaseModel):
|
27
|
+
"""ListVarModel"""
|
28
|
+
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
class ListObsParam(BaseModel):
|
33
|
+
"""ListObsModel"""
|
34
|
+
|
37
35
|
pass
|
38
36
|
|
39
|
-
class ListObsParams(BaseModel):
|
40
|
-
"""ListObsModel"""
|
41
|
-
pass
|
42
37
|
|
43
|
-
class
|
44
|
-
"""ListObsModel"""
|
45
|
-
|
46
|
-
|
47
|
-
description="gene names."
|
48
|
-
)
|
38
|
+
class VarNamesParam(BaseModel):
|
39
|
+
"""ListObsModel"""
|
40
|
+
|
41
|
+
var_names: List[str] = Field(default=None, description="gene names.")
|
49
42
|
|
50
43
|
|
51
|
-
class
|
44
|
+
class ConcatBaseParam(BaseModel):
|
52
45
|
"""Model for concatenating AnnData objects"""
|
53
|
-
|
54
|
-
axis: Literal[
|
55
|
-
default=
|
56
|
-
description="Which axis to concatenate along. 'obs' or 0 for observations, 'var' or 1 for variables."
|
46
|
+
|
47
|
+
axis: Literal["obs", 0, "var", 1] = Field(
|
48
|
+
default="obs",
|
49
|
+
description="Which axis to concatenate along. 'obs' or 0 for observations, 'var' or 1 for variables.",
|
57
50
|
)
|
58
|
-
join: Literal[
|
59
|
-
default=
|
60
|
-
description="How to align values when concatenating. If 'outer', the union of the other axis is taken. If 'inner', the intersection."
|
51
|
+
join: Literal["inner", "outer"] = Field(
|
52
|
+
default="inner",
|
53
|
+
description="How to align values when concatenating. If 'outer', the union of the other axis is taken. If 'inner', the intersection.",
|
61
54
|
)
|
62
|
-
merge: Optional[Literal[
|
55
|
+
merge: Optional[Literal["same", "unique", "first", "only"]] = Field(
|
63
56
|
default=None,
|
64
|
-
description="How elements not aligned to the axis being concatenated along are selected."
|
57
|
+
description="How elements not aligned to the axis being concatenated along are selected.",
|
65
58
|
)
|
66
|
-
uns_merge: Optional[Literal[
|
59
|
+
uns_merge: Optional[Literal["same", "unique", "first", "only"]] = Field(
|
67
60
|
default=None,
|
68
|
-
description="How the elements of .uns are selected. Uses the same set of strategies as the merge argument, except applied recursively."
|
61
|
+
description="How the elements of .uns are selected. Uses the same set of strategies as the merge argument, except applied recursively.",
|
69
62
|
)
|
70
63
|
label: Optional[str] = Field(
|
71
64
|
default=None,
|
72
|
-
description="label different adata, Column in axis annotation (i.e. .obs or .var) to place batch information in. "
|
65
|
+
description="label different adata, Column in axis annotation (i.e. .obs or .var) to place batch information in. ",
|
73
66
|
)
|
74
67
|
keys: Optional[List[str]] = Field(
|
75
68
|
default=None,
|
76
|
-
description="Names for each object being added. These values are used for column values for label or appended to the index if index_unique is not None."
|
69
|
+
description="Names for each object being added. These values are used for column values for label or appended to the index if index_unique is not None.",
|
77
70
|
)
|
78
71
|
index_unique: Optional[str] = Field(
|
79
72
|
default=None,
|
80
|
-
description="Whether to make the index unique by using the keys. If provided, this is the delimiter between '{orig_idx}{index_unique}{key}'."
|
73
|
+
description="Whether to make the index unique by using the keys. If provided, this is the delimiter between '{orig_idx}{index_unique}{key}'.",
|
81
74
|
)
|
82
75
|
fill_value: Optional[Any] = Field(
|
83
76
|
default=None,
|
84
|
-
description="When join='outer', this is the value that will be used to fill the introduced indices."
|
77
|
+
description="When join='outer', this is the value that will be used to fill the introduced indices.",
|
85
78
|
)
|
86
79
|
pairwise: bool = Field(
|
87
80
|
default=False,
|
88
|
-
description="Whether pairwise elements along the concatenated dimension should be included."
|
81
|
+
description="Whether pairwise elements along the concatenated dimension should be included.",
|
89
82
|
)
|
90
83
|
|
91
84
|
|
92
|
-
class
|
85
|
+
class DPTIROOTParam(BaseModel):
|
93
86
|
"""Input schema for setting the root cell for diffusion pseudotime."""
|
87
|
+
|
94
88
|
diffmap_key: str = Field(
|
95
89
|
default="X_diffmap",
|
96
|
-
description="Key for diffusion map coordinates stored in adata.obsm."
|
90
|
+
description="Key for diffusion map coordinates stored in adata.obsm.",
|
97
91
|
)
|
98
92
|
dimension: int = Field(
|
99
93
|
description="Dimension index to use for finding the root cell."
|
@@ -103,36 +97,27 @@ class DPTIROOTParams(BaseModel):
|
|
103
97
|
)
|
104
98
|
|
105
99
|
|
106
|
-
class
|
100
|
+
class CelltypeMapCellTypeParam(BaseModel):
|
107
101
|
"""Input schema for mapping cluster IDs to cell type names."""
|
108
|
-
|
109
|
-
|
110
|
-
)
|
111
|
-
added_key: str = Field(
|
112
|
-
description="Key to add to adata.obs for cell type names."
|
113
|
-
)
|
102
|
+
|
103
|
+
cluster_key: str = Field(description="Key in adata.obs containing cluster IDs.")
|
104
|
+
added_key: str = Field(description="Key to add to adata.obs for cell type names.")
|
114
105
|
mapping: Dict[str, str] = Field(
|
115
106
|
default=None,
|
116
|
-
description="Mapping Dictionary from cluster IDs to cell type names."
|
107
|
+
description="Mapping Dictionary from cluster IDs to cell type names.",
|
117
108
|
)
|
118
109
|
new_names: Optional[List[str]] = Field(
|
119
|
-
default=None,
|
120
|
-
description="a list of new cell type names."
|
110
|
+
default=None, description="a list of new cell type names."
|
121
111
|
)
|
122
|
-
|
123
112
|
|
124
113
|
|
125
|
-
class
|
114
|
+
class AddLayerParam(BaseModel):
|
126
115
|
"""Input schema for adding a layer to AnnData object."""
|
127
|
-
layer_name: str = Field(
|
128
|
-
description="Name of the layer to add to adata.layers."
|
129
|
-
)
|
130
|
-
|
131
116
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
117
|
+
layer_name: str = Field(description="Name of the layer to add to adata.layers.")
|
118
|
+
|
119
|
+
|
120
|
+
class QueryOpLogParam(BaseModel):
|
121
|
+
"""QueryOpLogModel"""
|
122
|
+
|
123
|
+
n: int = Field(default=10, description="Number of operations to return.")
|
scmcp_shared/server/__init__.py
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
from
|
5
|
-
from
|
6
|
-
import
|
3
|
+
from .auto import auto_mcp
|
4
|
+
from .rag import rag_mcp
|
5
|
+
from abcoder.server import nb_mcp
|
6
|
+
from . import preset
|
7
7
|
|
8
|
-
|
9
|
-
from .io import ScanpyIOMCP, io_mcp
|
10
|
-
from .util import ScanpyUtilMCP
|
11
|
-
from .pl import ScanpyPlottingMCP
|
12
|
-
from .pp import ScanpyPreprocessingMCP
|
13
|
-
from .tl import ScanpyToolsMCP
|
8
|
+
__all__ = ["auto_mcp", "rag_mcp", "nb_mcp", "preset"]
|
scmcp_shared/server/auto.py
CHANGED
@@ -6,24 +6,28 @@ from pydantic import Field
|
|
6
6
|
auto_mcp = FastMCP("SmartMCP-select-Server")
|
7
7
|
|
8
8
|
|
9
|
-
@auto_mcp.tool()
|
9
|
+
@auto_mcp.tool(tags={"auto"})
|
10
10
|
def search_tool(
|
11
|
-
task: str= Field(
|
11
|
+
task: str = Field(
|
12
|
+
description="The tasks or questions that needs to be solved using available tools"
|
13
|
+
),
|
12
14
|
):
|
13
|
-
"""search the tools that can be used to solve the user's tasks or questions"""
|
15
|
+
"""search the tools and get tool parameters that can be used to solve the user's tasks or questions"""
|
14
16
|
ctx = get_context()
|
15
17
|
fastmcp = ctx.fastmcp
|
16
|
-
|
18
|
+
if hasattr(fastmcp._tool_manager, "_all_tools"):
|
19
|
+
all_tools = fastmcp._tool_manager._all_tools
|
20
|
+
else:
|
21
|
+
all_tools = fastmcp._tool_manager._tools
|
17
22
|
auto_tools = fastmcp._tool_manager._tools
|
18
|
-
fastmcp._tool_manager._tools =
|
23
|
+
fastmcp._tool_manager._tools = all_tools
|
19
24
|
query = f"<task>{task}</task>\n"
|
20
25
|
for name in all_tools:
|
21
26
|
tool = all_tools[name]
|
22
27
|
query += f"<Tool>\n<name>{name}</name>\n<description>{tool.description}</description>\n</Tool>\n"
|
23
|
-
|
24
28
|
results = select_tool(query)
|
25
29
|
tool_list = []
|
26
|
-
for tool in results:
|
30
|
+
for tool in results.tools:
|
27
31
|
tool = tool.model_dump()
|
28
32
|
tool["parameters"] = all_tools[tool["name"]].parameters
|
29
33
|
tool_list.append(tool)
|
@@ -31,17 +35,17 @@ def search_tool(
|
|
31
35
|
return tool_list
|
32
36
|
|
33
37
|
|
34
|
-
@auto_mcp.tool()
|
38
|
+
@auto_mcp.tool(tags={"auto"})
|
35
39
|
async def run_tool(
|
36
|
-
name: str= Field(description="The name of the tool to run"),
|
37
|
-
parameter: dict = Field(description="The parameters to pass to the tool")
|
40
|
+
name: str = Field(description="The name of the tool to run"),
|
41
|
+
parameter: dict = Field(description="The parameters to pass to the tool"),
|
38
42
|
):
|
39
43
|
"""run the tool with the given name and parameters. Only start call the tool when last tool is finished."""
|
40
44
|
ctx = get_context()
|
41
45
|
fastmcp = ctx.fastmcp
|
42
46
|
all_tools = fastmcp._tool_manager._all_tools
|
43
47
|
auto_tools = fastmcp._tool_manager._tools
|
44
|
-
fastmcp._tool_manager._tools =
|
48
|
+
fastmcp._tool_manager._tools = all_tools
|
45
49
|
|
46
50
|
try:
|
47
51
|
result = await fastmcp._tool_manager.call_tool(name, parameter)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from .io import ScanpyIOMCP, io_mcp
|
2
|
+
from .pl import ScanpyPlottingMCP
|
3
|
+
from .pp import ScanpyPreprocessingMCP
|
4
|
+
from .tl import ScanpyToolsMCP
|
5
|
+
from .util import ScanpyUtilMCP
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"io_mcp",
|
9
|
+
"ScanpyIOMCP",
|
10
|
+
"ScanpyPlottingMCP",
|
11
|
+
"ScanpyPreprocessingMCP",
|
12
|
+
"ScanpyToolsMCP",
|
13
|
+
"ScanpyUtilMCP",
|
14
|
+
]
|
@@ -1,23 +1,25 @@
|
|
1
|
-
import os
|
2
|
-
import inspect
|
3
1
|
from pathlib import Path
|
4
2
|
import scanpy as sc
|
5
|
-
from fastmcp import FastMCP, Context
|
6
3
|
from fastmcp.tools.tool import Tool
|
7
4
|
from fastmcp.exceptions import ToolError
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from .
|
5
|
+
from scmcp_shared.schema.preset import AdataInfo
|
6
|
+
from scmcp_shared.schema.preset.io import *
|
7
|
+
from scmcp_shared.util import filter_args, forward_request, get_ads
|
8
|
+
from scmcp_shared.mcp_base import BaseMCP
|
12
9
|
|
13
10
|
|
14
11
|
class ScanpyIOMCP(BaseMCP):
|
15
|
-
def __init__(
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
include_tools: list = None,
|
15
|
+
exclude_tools: list = None,
|
16
|
+
AdataInfo=AdataInfo,
|
17
|
+
):
|
16
18
|
"""Initialize ScanpyIOMCP with optional tool filtering."""
|
17
19
|
super().__init__("SCMCP-IO-Server", include_tools, exclude_tools, AdataInfo)
|
18
20
|
|
19
21
|
def _tool_read(self):
|
20
|
-
def _read(request:
|
22
|
+
def _read(request: ReadParam, adinfo: self.AdataInfo = self.AdataInfo()):
|
21
23
|
"""
|
22
24
|
Read data from 10X directory or various file formats (h5ad, 10x, text files, etc.).
|
23
25
|
"""
|
@@ -34,7 +36,7 @@ class ScanpyIOMCP(BaseMCP):
|
|
34
36
|
elif file.is_file():
|
35
37
|
func_kwargs = filter_args(request, sc.read)
|
36
38
|
adata = sc.read(**func_kwargs)
|
37
|
-
if
|
39
|
+
if kwargs.get("transpose", False):
|
38
40
|
adata = adata.T
|
39
41
|
else:
|
40
42
|
raise FileNotFoundError(f"{kwargs['filename']} does not exist")
|
@@ -44,7 +46,7 @@ class ScanpyIOMCP(BaseMCP):
|
|
44
46
|
ads.active_id = adinfo.sampleid
|
45
47
|
else:
|
46
48
|
ads.active_id = f"adata{len(ads.adata_dic[adinfo.adtype])}"
|
47
|
-
|
49
|
+
|
48
50
|
adata.layers["counts"] = adata.X
|
49
51
|
adata.var_names_make_unique()
|
50
52
|
adata.obs_names_make_unique()
|
@@ -52,30 +54,31 @@ class ScanpyIOMCP(BaseMCP):
|
|
52
54
|
ads.set_adata(adata, adinfo=adinfo)
|
53
55
|
return [
|
54
56
|
{
|
55
|
-
"sampleid": adinfo.sampleid or ads.active_id,
|
56
|
-
"adtype": adinfo.adtype,
|
57
|
+
"sampleid": adinfo.sampleid or ads.active_id,
|
58
|
+
"adtype": adinfo.adtype,
|
57
59
|
"adata": adata,
|
58
60
|
"adata.obs_names[:10]": adata.obs_names[:10],
|
59
61
|
"adata.var_names[:10]": adata.var_names[:10],
|
60
|
-
"notice": "check obs_names and var_names. transpose the data if needed"
|
62
|
+
"notice": "check obs_names and var_names. transpose the data if needed",
|
61
63
|
}
|
62
|
-
|
64
|
+
]
|
63
65
|
except ToolError as e:
|
64
66
|
raise ToolError(e)
|
65
67
|
except Exception as e:
|
66
|
-
if hasattr(e,
|
68
|
+
if hasattr(e, "__context__") and e.__context__:
|
67
69
|
raise ToolError(e.__context__)
|
68
70
|
else:
|
69
71
|
raise ToolError(e)
|
70
|
-
|
72
|
+
|
73
|
+
return Tool.from_function(_read, name="read", enabled=True)
|
71
74
|
|
72
75
|
def _tool_write(self):
|
73
|
-
def _write(request:
|
76
|
+
def _write(request: WriteParam, adinfo: self.AdataInfo = self.AdataInfo()):
|
74
77
|
"""save adata into a file."""
|
75
78
|
try:
|
76
79
|
res = forward_request("io_write", request, adinfo)
|
77
80
|
if res is not None:
|
78
|
-
return res
|
81
|
+
return res
|
79
82
|
ads = get_ads()
|
80
83
|
adata = ads.get_adata(adinfo=adinfo)
|
81
84
|
kwargs = request.model_dump()
|
@@ -84,12 +87,13 @@ class ScanpyIOMCP(BaseMCP):
|
|
84
87
|
except ToolError as e:
|
85
88
|
raise ToolError(e)
|
86
89
|
except Exception as e:
|
87
|
-
if hasattr(e,
|
90
|
+
if hasattr(e, "__context__") and e.__context__:
|
88
91
|
raise ToolError(e.__context__)
|
89
92
|
else:
|
90
93
|
raise ToolError(e)
|
91
|
-
|
94
|
+
|
95
|
+
return Tool.from_function(_write, name="write", enabled=True)
|
92
96
|
|
93
97
|
|
94
98
|
# Create an instance of the class
|
95
|
-
io_mcp = ScanpyIOMCP().mcp
|
99
|
+
io_mcp = ScanpyIOMCP().mcp
|