czty commited on
Commit
eb88e90
·
verified ·
1 Parent(s): 96d6cfe

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/Dockerfile +40 -0
  2. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/adapterremoval_server.py +283 -0
  3. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/adapterremoval_shim_server.py +45 -0
  4. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/requirements.txt +1 -0
  5. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/docker-compose.yml +22 -0
  6. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/environment.yaml +10 -0
  7. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/requirements.txt +2 -0
  8. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/Dockerfile +40 -0
  9. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/alfred_server.py +630 -0
  10. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/alfred_shim_server.py +45 -0
  11. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/requirements.txt +1 -0
  12. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/docker-compose.yml +22 -0
  13. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/environment.yaml +10 -0
  14. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/requirements.txt +2 -0
  15. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/Dockerfile +40 -0
  16. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/app/anansescanpy_server.py +270 -0
  17. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/app/anansescanpy_shim_server.py +45 -0
  18. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/docker-compose.yml +22 -0
  19. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/environment.yaml +10 -0
  20. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/requirements.txt +2 -0
  21. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/Dockerfile +40 -0
  22. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/app/aria2_server.py +307 -0
  23. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/app/aria2_shim_server.py +45 -0
  24. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/docker-compose.yml +22 -0
  25. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/environment.yaml +10 -0
  26. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/requirements.txt +2 -0
  27. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/Dockerfile +40 -0
  28. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/app/bcbio-nextgen_server.py +268 -0
  29. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/app/bcbio-nextgen_shim_server.py +45 -0
  30. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/docker-compose.yml +22 -0
  31. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/environment.yaml +10 -0
  32. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/requirements.txt +2 -0
  33. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/Dockerfile +40 -0
  34. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/__pycache__/bioconda-utils_server.cpython-310.pyc +0 -0
  35. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/bioconda-utils_server.py +490 -0
  36. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/bioconda-utils_shim_server.py +45 -0
  37. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/requirements.txt +1 -0
  38. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/docker-compose.yml +22 -0
  39. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/environment.yaml +10 -0
  40. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/requirements.txt +2 -0
  41. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/Dockerfile +40 -0
  42. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/bioconductor-banksy_server.py +659 -0
  43. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/bioconductor-banksy_shim_server.py +45 -0
  44. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/requirements.txt +1 -0
  45. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/docker-compose.yml +22 -0
  46. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/environment.yaml +10 -0
  47. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/requirements.txt +2 -0
  48. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/Dockerfile +40 -0
  49. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_server.py +464 -0
  50. BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_shim_server.py +45 -0
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install adapterremoval via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda adapterremoval -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY app/adapterremoval_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/adapterremoval_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/adapterremoval_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/adapterremoval_server.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import Optional, List, Dict, Any
4
+
5
+ @mcp.tool()
6
+ def adapter_removal(
7
+ file1: str,
8
+ file2: Optional[str] = None,
9
+ adapter1: str = "AGATCGGAAGAGCACACGTCTGAACTCCAGTCAC",
10
+ adapter2: str = "AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGTAGATCTCGGTGGTCGCCGTATCATT",
11
+ adapter_list: Optional[str] = None,
12
+ basename: str = "adapterremoval_output",
13
+ trimns: bool = False,
14
+ trimqualities: bool = False,
15
+ minquality: int = 2,
16
+ minlength: int = 15,
17
+ maxlength: int = 0,
18
+ collapse: bool = False,
19
+ collapse_deterministic: bool = False,
20
+ mismatchrate: float = 0.333,
21
+ minadapteroverlap: int = 0,
22
+ gzip: bool = False,
23
+ bzip2: bool = False,
24
+ threads: int = 1,
25
+ interleaved: bool = False,
26
+ interleaved_output: bool = False,
27
+ qualitybase: int = 33,
28
+ qualitymax: int = 41,
29
+ mate_separator: str = "/",
30
+ trim_low_complexity: bool = False,
31
+ ):
32
+ """
33
+ Perform adapter trimming, quality trimming, and optional merging of paired-end reads using AdapterRemoval v2.
34
+
35
+ Args:
36
+ file1: Path to the first input FASTQ file (or interleaved file).
37
+ file2: Path to the second input FASTQ file (optional).
38
+ adapter1: Sequence of the first adapter.
39
+ adapter2: Sequence of the second adapter.
40
+ adapter_list: Path to a file containing a list of adapters (one per line).
41
+ basename: Prefix for all output files.
42
+ trimns: If True, trim ambiguous bases (N) from the 5' and 3' ends.
43
+ trimqualities: If True, trim low-quality bases from the 5' and 3' ends.
44
+ minquality: Minimum quality score for trimming (default 2).
45
+ minlength: Minimum read length after trimming (default 15).
46
+ maxlength: Maximum read length; reads longer than this are discarded (0 = disabled).
47
+ collapse: If True, merge overlapping paired-end reads into a single consensus sequence.
48
+ collapse_deterministic: If True, use deterministic merging for overlapping reads.
49
+ mismatchrate: Maximum allowed fraction of mismatches in the overlapping region (default 0.333).
50
+ minadapteroverlap: Minimum overlap between read and adapter (default 0).
51
+ gzip: If True, compress output files using gzip.
52
+ bzip2: If True, compress output files using bzip2.
53
+ threads: Number of threads to use.
54
+ interleaved: If True, input file1 is treated as an interleaved paired-end file.
55
+ interleaved_output: If True, output paired-end reads in a single interleaved file.
56
+ qualitybase: Phred quality score offset (33 or 64).
57
+ qualitymax: Maximum Phred quality score (default 41).
58
+ mate_separator: Character separating mate number in read names (default '/').
59
+ trim_low_complexity: If True, trim low complexity (e.g. poly-A) sequences.
60
+ """
61
+ # Input validation
62
+ f1_path = Path(file1)
63
+ if not f1_path.exists():
64
+ raise FileNotFoundError(f"Input file1 not found: {file1}")
65
+
66
+ cmd = ["AdapterRemoval", "--file1", str(f1_path)]
67
+
68
+ if file2:
69
+ f2_path = Path(file2)
70
+ if not f2_path.exists():
71
+ raise FileNotFoundError(f"Input file2 not found: {file2}")
72
+ cmd.extend(["--file2", str(f2_path)])
73
+
74
+ if adapter_list:
75
+ alist_path = Path(adapter_list)
76
+ if not alist_path.exists():
77
+ raise FileNotFoundError(f"Adapter list file not found: {adapter_list}")
78
+ cmd.extend(["--adapter-list", str(alist_path)])
79
+ else:
80
+ cmd.extend(["--adapter1", adapter1, "--adapter2", adapter2])
81
+
82
+ # Parameters
83
+ cmd.extend(["--basename", basename])
84
+ cmd.extend(["--minquality", str(minquality)])
85
+ cmd.extend(["--minlength", str(minlength)])
86
+ cmd.extend(["--mismatchrate", str(mismatchrate)])
87
+ cmd.extend(["--minadapteroverlap", str(minadapteroverlap)])
88
+ cmd.extend(["--threads", str(threads)])
89
+ cmd.extend(["--qualitybase", str(qualitybase)])
90
+ cmd.extend(["--qualitymax", str(qualitymax)])
91
+ cmd.extend(["--mate-separator", mate_separator])
92
+
93
+ if trimns: cmd.append("--trimns")
94
+ if trimqualities: cmd.append("--trimqualities")
95
+ if collapse: cmd.append("--collapse")
96
+ if collapse_deterministic: cmd.append("--collapse-deterministic")
97
+ if gzip: cmd.append("--gzip")
98
+ if bzip2: cmd.append("--bzip2")
99
+ if interleaved: cmd.append("--interleaved")
100
+ if interleaved_output: cmd.append("--interleaved-output")
101
+ if trim_low_complexity: cmd.append("--trim-low-complexity")
102
+ if maxlength > 0:
103
+ cmd.extend(["--maxlength", str(maxlength)])
104
+
105
+ try:
106
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
107
+
108
+ # Identify potential output files based on basename and flags
109
+ ext = ".gz" if gzip else (".bz2" if bzip2 else "")
110
+ output_files = [f"{basename}.settings"]
111
+
112
+ # Standard outputs
113
+ if file2 or interleaved:
114
+ output_files.extend([
115
+ f"{basename}.pair1.truncated{ext}",
116
+ f"{basename}.pair2.truncated{ext}",
117
+ f"{basename}.singleton.truncated{ext}"
118
+ ])
119
+ if collapse or collapse_deterministic:
120
+ output_files.extend([
121
+ f"{basename}.collapsed{ext}",
122
+ f"{basename}.collapsed.truncated{ext}"
123
+ ])
124
+ else:
125
+ output_files.append(f"{basename}.truncated{ext}")
126
+
127
+ # Filter for files that actually exist
128
+ existing_outputs = [f for f in output_files if Path(f).exists()]
129
+
130
+ return {
131
+ "command_executed": " ".join(cmd),
132
+ "stdout": result.stdout,
133
+ "stderr": result.stderr,
134
+ "output_files": existing_outputs
135
+ }
136
+ except subprocess.CalledProcessError as e:
137
+ return {
138
+ "command_executed": " ".join(cmd),
139
+ "error": str(e),
140
+ "stdout": e.stdout,
141
+ "stderr": e.stderr
142
+ }
143
+
144
+ @mcp.tool()
145
+ def adapter_removal_identify_adapters(
146
+ file1: str,
147
+ file2: str,
148
+ threads: int = 1,
149
+ interleaved: bool = False,
150
+ qualitybase: int = 33,
151
+ ):
152
+ """
153
+ Attempt to identify adapter sequences by looking for consensus overlaps in paired-end reads.
154
+
155
+ Args:
156
+ file1: Path to the first input FASTQ file.
157
+ file2: Path to the second input FASTQ file.
158
+ threads: Number of threads to use.
159
+ interleaved: If True, input file1 is treated as an interleaved paired-end file.
160
+ qualitybase: Phred quality score offset (33 or 64).
161
+ """
162
+ f1_path = Path(file1)
163
+ f2_path = Path(file2)
164
+
165
+ if not f1_path.exists():
166
+ raise FileNotFoundError(f"Input file1 not found: {file1}")
167
+ if not f2_path.exists() and not interleaved:
168
+ raise FileNotFoundError(f"Input file2 not found: {file2}")
169
+
170
+ cmd = [
171
+ "AdapterRemoval",
172
+ "--identify-adapters",
173
+ "--file1", str(f1_path),
174
+ "--threads", str(threads),
175
+ "--qualitybase", str(qualitybase)
176
+ ]
177
+
178
+ if not interleaved:
179
+ cmd.extend(["--file2", str(f2_path)])
180
+ else:
181
+ cmd.append("--interleaved")
182
+
183
+ try:
184
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
185
+ return {
186
+ "command_executed": " ".join(cmd),
187
+ "stdout": result.stdout,
188
+ "stderr": result.stderr,
189
+ "identified_adapters": result.stdout # Usually printed to stdout
190
+ }
191
+ except subprocess.CalledProcessError as e:
192
+ return {
193
+ "command_executed": " ".join(cmd),
194
+ "error": str(e),
195
+ "stdout": e.stdout,
196
+ "stderr": e.stderr
197
+ }
198
+
199
+ @mcp.tool()
200
+ def adapter_removal_demultiplex(
201
+ barcode_list: str,
202
+ file1: str,
203
+ file2: Optional[str] = None,
204
+ basename: str = "demux_output",
205
+ barcode_mm: int = 1,
206
+ barcode_mm_r1: Optional[int] = None,
207
+ barcode_mm_r2: Optional[int] = None,
208
+ gzip: bool = False,
209
+ threads: int = 1,
210
+ interleaved: bool = False,
211
+ ):
212
+ """
213
+ Demultiplex reads based on a list of barcodes using AdapterRemoval v2.
214
+
215
+ Args:
216
+ barcode_list: Path to a file containing barcodes (Format: Name Barcode1 Barcode2).
217
+ file1: Path to the first input FASTQ file.
218
+ file2: Path to the second input FASTQ file (optional).
219
+ basename: Prefix for all output files.
220
+ barcode_mm: Max number of mismatches allowed in barcodes (default 1).
221
+ barcode_mm_r1: Max mismatches for barcode 1 (overrides barcode_mm).
222
+ barcode_mm_r2: Max mismatches for barcode 2 (overrides barcode_mm).
223
+ gzip: If True, compress output files using gzip.
224
+ threads: Number of threads to use.
225
+ interleaved: If True, input file1 is treated as an interleaved paired-end file.
226
+ """
227
+ blist_path = Path(barcode_list)
228
+ f1_path = Path(file1)
229
+
230
+ if not blist_path.exists():
231
+ raise FileNotFoundError(f"Barcode list file not found: {barcode_list}")
232
+ if not f1_path.exists():
233
+ raise FileNotFoundError(f"Input file1 not found: {file1}")
234
+
235
+ cmd = [
236
+ "AdapterRemoval",
237
+ "--barcode-list", str(blist_path),
238
+ "--file1", str(f1_path),
239
+ "--basename", basename,
240
+ "--threads", str(threads)
241
+ ]
242
+
243
+ if file2:
244
+ f2_path = Path(file2)
245
+ if not f2_path.exists():
246
+ raise FileNotFoundError(f"Input file2 not found: {file2}")
247
+ cmd.extend(["--file2", str(f2_path)])
248
+
249
+ if interleaved:
250
+ cmd.append("--interleaved")
251
+
252
+ if gzip:
253
+ cmd.append("--gzip")
254
+
255
+ if barcode_mm_r1 is not None:
256
+ cmd.extend(["--barcode-mm-r1", str(barcode_mm_r1)])
257
+ if barcode_mm_r2 is not None:
258
+ cmd.extend(["--barcode-mm-r2", str(barcode_mm_r2)])
259
+ if barcode_mm_r1 is None and barcode_mm_r2 is None:
260
+ cmd.extend(["--barcode-mm", str(barcode_mm)])
261
+
262
+ try:
263
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
264
+
265
+ # Demultiplexing creates many files based on the barcode names in the list
266
+ # We'll return the settings file and a note about the outputs
267
+ ext = ".gz" if gzip else ""
268
+ settings_file = f"{basename}.settings"
269
+
270
+ return {
271
+ "command_executed": " ".join(cmd),
272
+ "stdout": result.stdout,
273
+ "stderr": result.stderr,
274
+ "settings_file": settings_file if Path(settings_file).exists() else "Not found",
275
+ "info": "Demultiplexed files are created with the prefix specified in the barcode list."
276
+ }
277
+ except subprocess.CalledProcessError as e:
278
+ return {
279
+ "command_executed": " ".join(cmd),
280
+ "error": str(e),
281
+ "stdout": e.stdout,
282
+ "stderr": e.stderr
283
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/adapterremoval_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/adapterremoval_server.py')
11
+ SERVER_NAME = 'biosci_adapterremoval'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/app/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-adapterremoval:
5
+ build: .
6
+ image: mcp-adapterremoval:latest
7
+ container_name: mcp-adapterremoval
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=adapterremoval
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - adapterremoval
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_adapterremoval/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install alfred via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda alfred -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY app/alfred_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/alfred_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/alfred_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/alfred_server.py ADDED
@@ -0,0 +1,630 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import List, Literal, Optional
5
+
6
+ # MCP-related decorators are assumed to be available in the environment.
7
+ #
8
+ # For local testing, you can create a dummy decorator:
9
+ #
10
+ # def mcp_tool_dummy(*args, **kwargs):
11
+ # def decorator(func):
12
+ # return func
13
+ # return decorator
14
+ #
15
+ # mcp = type("mcp", (), {"tool": mcp_tool_dummy})
16
+
17
+
18
+ @mcp.tool()
19
+ def alfred_stats(
20
+ in_bam: Path,
21
+ ref: Path,
22
+ outfile: Optional[Path] = None,
23
+ bed: Optional[Path] = None,
24
+ threads: int = 1,
25
+ minmapq: int = 0,
26
+ flag: int = 0,
27
+ fflag: int = 1540,
28
+ coverage: Optional[Path] = None,
29
+ dist: Optional[Path] = None,
30
+ sampleid: Optional[str] = None,
31
+ libid: Optional[str] = None,
32
+ readgroup: Optional[str] = None,
33
+ uncompressed: bool = False,
34
+ ):
35
+ """
36
+ Compute alignment summary statistics for a BAM file.
37
+
38
+ Args:
39
+ in_bam: Input BAM file.
40
+ ref: Reference FASTA file.
41
+ outfile: Output file for alignment metrics (e.g., metrics.tsv.gz). Defaults to stdout.
42
+ bed: BED file with regions of interest.
43
+ threads: Number of threads to use.
44
+ minmapq: Minimum mapping quality for reads to be considered.
45
+ flag: Required SAM flag.
46
+ fflag: Filtering SAM flag (reads with these flags will be ignored).
47
+ coverage: Output file for coverage statistics (e.g., cov.txt.gz).
48
+ dist: Output file for insert size distribution (e.g., dist.txt.gz).
49
+ sampleid: Sample ID to be used in the output.
50
+ libid: Library ID to be used in the output.
51
+ readgroup: Read-group ID to be used in the output.
52
+ uncompressed: Write uncompressed output files.
53
+ """
54
+ if not in_bam.exists():
55
+ raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
56
+ if not ref.exists():
57
+ raise FileNotFoundError(f"Reference FASTA file not found: {ref}")
58
+ if bed and not bed.exists():
59
+ raise FileNotFoundError(f"BED file not found: {bed}")
60
+ if threads < 1:
61
+ raise ValueError("Number of threads must be at least 1.")
62
+
63
+ cmd = ["alfred", "stats", "-r", str(ref)]
64
+ output_files = []
65
+
66
+ if outfile:
67
+ cmd.extend(["-o", str(outfile)])
68
+ output_files.append(str(outfile))
69
+ if bed:
70
+ cmd.extend(["-b", str(bed)])
71
+ if threads > 1:
72
+ cmd.extend(["-p", str(threads)])
73
+ if minmapq != 0:
74
+ cmd.extend(["-m", str(minmapq)])
75
+ if flag != 0:
76
+ cmd.extend(["-f", str(flag)])
77
+ if fflag != 1540:
78
+ cmd.extend(["-F", str(fflag)])
79
+ if coverage:
80
+ cmd.extend(["-c", str(coverage)])
81
+ output_files.append(str(coverage))
82
+ if dist:
83
+ cmd.extend(["-d", str(dist)])
84
+ output_files.append(str(dist))
85
+ if sampleid:
86
+ cmd.extend(["-s", sampleid])
87
+ if libid:
88
+ cmd.extend(["-l", libid])
89
+ if readgroup:
90
+ cmd.extend(["-g", readgroup])
91
+ if uncompressed:
92
+ cmd.append("-u")
93
+
94
+ cmd.append(str(in_bam))
95
+
96
+ try:
97
+ result = subprocess.run(
98
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
99
+ )
100
+ return {
101
+ "command_executed": " ".join(cmd),
102
+ "stdout": result.stdout,
103
+ "stderr": result.stderr,
104
+ "output_files": output_files,
105
+ }
106
+ except subprocess.CalledProcessError as e:
107
+ return {
108
+ "command_executed": " ".join(cmd),
109
+ "stdout": e.stdout,
110
+ "stderr": e.stderr,
111
+ "error": "Alfred stats failed",
112
+ "return_code": e.returncode,
113
+ }
114
+
115
+
116
+ @mcp.tool()
117
+ def alfred_count(
118
+ in_bam: Path,
119
+ ref: Path,
120
+ bed: Optional[Path] = None,
121
+ gtf: Optional[Path] = None,
122
+ outfile: Optional[Path] = None,
123
+ threads: int = 1,
124
+ minmapq: int = 0,
125
+ flag: int = 0,
126
+ fflag: int = 1540,
127
+ sampleid: Optional[str] = None,
128
+ libid: Optional[str] = None,
129
+ readgroup: Optional[str] = None,
130
+ uncompressed: bool = False,
131
+ feature: str = "exon",
132
+ id_attribute: str = "gene_id",
133
+ stranded: Literal[0, 1, 2] = 0,
134
+ antisense: bool = False,
135
+ ):
136
+ """
137
+ Count reads in genomic features from a BAM file.
138
+
139
+ Args:
140
+ in_bam: Input BAM file.
141
+ ref: Reference FASTA file.
142
+ bed: BED file with features. Provide either 'bed' or 'gtf'.
143
+ gtf: GTF file with features. Provide either 'bed' or 'gtf'.
144
+ outfile: Output file for feature counts (e.g., counts.tsv.gz). Defaults to stdout.
145
+ threads: Number of threads to use.
146
+ minmapq: Minimum mapping quality.
147
+ flag: Required SAM flag.
148
+ fflag: Filtering SAM flag.
149
+ sampleid: Sample ID.
150
+ libid: Library ID.
151
+ readgroup: Read-group ID.
152
+ uncompressed: Write uncompressed output.
153
+ feature: GTF feature to count (e.g., 'exon').
154
+ id_attribute: GTF identifier to aggregate by (e.g., 'gene_id').
155
+ stranded: Strandedness (0: unstranded, 1: forward, 2: reverse).
156
+ antisense: Count antisense reads.
157
+ """
158
+ if not in_bam.exists():
159
+ raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
160
+ if not ref.exists():
161
+ raise FileNotFoundError(f"Reference FASTA file not found: {ref}")
162
+ if bed and gtf:
163
+ raise ValueError("Parameters 'bed' and 'gtf' are mutually exclusive.")
164
+ if not bed and not gtf:
165
+ raise ValueError("Either 'bed' or 'gtf' must be provided.")
166
+ if bed and not bed.exists():
167
+ raise FileNotFoundError(f"BED file not found: {bed}")
168
+ if gtf and not gtf.exists():
169
+ raise FileNotFoundError(f"GTF file not found: {gtf}")
170
+ if threads < 1:
171
+ raise ValueError("Number of threads must be at least 1.")
172
+
173
+ cmd = ["alfred", "count", "-r", str(ref)]
174
+ output_files = []
175
+
176
+ if bed:
177
+ cmd.extend(["-b", str(bed)])
178
+ if gtf:
179
+ cmd.extend(["-j", str(gtf)])
180
+ if outfile:
181
+ cmd.extend(["-o", str(outfile)])
182
+ output_files.append(str(outfile))
183
+ if threads > 1:
184
+ cmd.extend(["-p", str(threads)])
185
+ if minmapq != 0:
186
+ cmd.extend(["-m", str(minmapq)])
187
+ if flag != 0:
188
+ cmd.extend(["-f", str(flag)])
189
+ if fflag != 1540:
190
+ cmd.extend(["-F", str(fflag)])
191
+ if sampleid:
192
+ cmd.extend(["-s", sampleid])
193
+ if libid:
194
+ cmd.extend(["-l", libid])
195
+ if readgroup:
196
+ cmd.extend(["-g", readgroup])
197
+ if uncompressed:
198
+ cmd.append("-u")
199
+ if feature != "exon":
200
+ cmd.extend(["-e", feature])
201
+ if id_attribute != "gene_id":
202
+ cmd.extend(["-i", id_attribute])
203
+ if stranded != 0:
204
+ cmd.extend(["-a", str(stranded)])
205
+ if antisense:
206
+ cmd.append("-z")
207
+
208
+ cmd.append(str(in_bam))
209
+
210
+ try:
211
+ result = subprocess.run(
212
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
213
+ )
214
+ return {
215
+ "command_executed": " ".join(cmd),
216
+ "stdout": result.stdout,
217
+ "stderr": result.stderr,
218
+ "output_files": output_files,
219
+ }
220
+ except subprocess.CalledProcessError as e:
221
+ return {
222
+ "command_executed": " ".join(cmd),
223
+ "stdout": e.stdout,
224
+ "stderr": e.stderr,
225
+ "error": "Alfred count failed",
226
+ "return_code": e.returncode,
227
+ }
228
+
229
+
230
+ @mcp.tool()
231
+ def alfred_annotate(
232
+ in_vcf: Path,
233
+ in_bam: Path,
234
+ outfile: Path,
235
+ ref: Path,
236
+ bed: Optional[Path] = None,
237
+ gtf: Optional[Path] = None,
238
+ threads: int = 1,
239
+ minmapq: int = 0,
240
+ flag: int = 0,
241
+ fflag: int = 1540,
242
+ feature: str = "exon",
243
+ id_attribute: str = "gene_id",
244
+ annotation_type: Literal["INFO", "FORMAT"] = "FORMAT",
245
+ field: str = "FE",
246
+ ):
247
+ """
248
+ Annotate variants in a VCF/BCF file with feature overlaps from a BAM file.
249
+
250
+ Args:
251
+ in_vcf: Input VCF/BCF file.
252
+ in_bam: Input BAM file.
253
+ outfile: Output VCF/BCF file.
254
+ ref: Reference FASTA file.
255
+ bed: BED file with features. Provide either 'bed' or 'gtf'.
256
+ gtf: GTF file with features. Provide either 'bed' or 'gtf'.
257
+ threads: Number of threads to use.
258
+ minmapq: Minimum mapping quality.
259
+ flag: Required SAM flag.
260
+ fflag: Filtering SAM flag.
261
+ feature: GTF feature to count (e.g., 'exon').
262
+ id_attribute: GTF identifier to aggregate by (e.g., 'gene_id').
263
+ annotation_type: Annotation type ('INFO' or 'FORMAT').
264
+ field: Annotation field name.
265
+ """
266
+ if not in_vcf.exists():
267
+ raise FileNotFoundError(f"Input VCF/BCF file not found: {in_vcf}")
268
+ if not in_bam.exists():
269
+ raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
270
+ if not ref.exists():
271
+ raise FileNotFoundError(f"Reference FASTA file not found: {ref}")
272
+ if bed and gtf:
273
+ raise ValueError("Parameters 'bed' and 'gtf' are mutually exclusive.")
274
+ if not bed and not gtf:
275
+ raise ValueError("Either 'bed' or 'gtf' must be provided.")
276
+ if bed and not bed.exists():
277
+ raise FileNotFoundError(f"BED file not found: {bed}")
278
+ if gtf and not gtf.exists():
279
+ raise FileNotFoundError(f"GTF file not found: {gtf}")
280
+ if threads < 1:
281
+ raise ValueError("Number of threads must be at least 1.")
282
+
283
+ cmd = ["alfred", "annotate", "-o", str(outfile), "-r", str(ref)]
284
+ output_files = [str(outfile)]
285
+
286
+ if bed:
287
+ cmd.extend(["-b", str(bed)])
288
+ if gtf:
289
+ cmd.extend(["-j", str(gtf)])
290
+ if threads > 1:
291
+ cmd.extend(["-p", str(threads)])
292
+ if minmapq != 0:
293
+ cmd.extend(["-m", str(minmapq)])
294
+ if flag != 0:
295
+ cmd.extend(["-f", str(flag)])
296
+ if fflag != 1540:
297
+ cmd.extend(["-F", str(fflag)])
298
+ if feature != "exon":
299
+ cmd.extend(["-e", feature])
300
+ if id_attribute != "gene_id":
301
+ cmd.extend(["-i", id_attribute])
302
+ if annotation_type != "FORMAT":
303
+ cmd.extend(["-t", annotation_type])
304
+ if field != "FE":
305
+ cmd.extend(["-a", field])
306
+
307
+ cmd.extend([str(in_vcf), str(in_bam)])
308
+
309
+ try:
310
+ result = subprocess.run(
311
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
312
+ )
313
+ return {
314
+ "command_executed": " ".join(cmd),
315
+ "stdout": result.stdout,
316
+ "stderr": result.stderr,
317
+ "output_files": output_files,
318
+ }
319
+ except subprocess.CalledProcessError as e:
320
+ return {
321
+ "command_executed": " ".join(cmd),
322
+ "stdout": e.stdout,
323
+ "stderr": e.stderr,
324
+ "error": "Alfred annotate failed",
325
+ "return_code": e.returncode,
326
+ }
327
+
328
+
329
+ @mcp.tool()
330
+ def alfred_qc(
331
+ input_bams: List[Path],
332
+ outfile: Path,
333
+ ref: Path,
334
+ bed: Optional[Path] = None,
335
+ gtf: Optional[Path] = None,
336
+ threads: int = 1,
337
+ minmapq: int = 0,
338
+ flag: int = 0,
339
+ fflag: int = 1540,
340
+ contigs: Optional[str] = None,
341
+ feature: str = "exon",
342
+ id_attribute: str = "gene_id",
343
+ stranded: Literal[0, 1, 2] = 0,
344
+ antisense: bool = False,
345
+ uncompressed: bool = False,
346
+ sites: Optional[Path] = None,
347
+ genome: Optional[Path] = None,
348
+ ):
349
+ """
350
+ Generate a multi-sample QC report from one or more BAM files.
351
+
352
+ Args:
353
+ input_bams: List of input BAM files.
354
+ outfile: Output file for QC metrics (e.g., out.qc.json.gz).
355
+ ref: Reference FASTA file.
356
+ bed: BED file with regions of interest.
357
+ gtf: GTF file with features.
358
+ threads: Number of threads to use.
359
+ minmapq: Minimum mapping quality.
360
+ flag: Required SAM flag.
361
+ fflag: Filtering SAM flag.
362
+ contigs: Comma-separated list of contigs to include.
363
+ feature: GTF feature to count (e.g., 'exon').
364
+ id_attribute: GTF identifier to aggregate by (e.g., 'gene_id').
365
+ stranded: Strandedness (0: unstranded, 1: forward, 2: reverse).
366
+ antisense: Count antisense reads.
367
+ uncompressed: Write uncompressed output.
368
+ sites: VCF/BCF file with sites of interest.
369
+ genome: Genome accessibility file.
370
+ """
371
+ if not input_bams:
372
+ raise ValueError("At least one input BAM file is required.")
373
+ for bam in input_bams:
374
+ if not bam.exists():
375
+ raise FileNotFoundError(f"Input BAM file not found: {bam}")
376
+ if not ref.exists():
377
+ raise FileNotFoundError(f"Reference FASTA file not found: {ref}")
378
+ if bed and not bed.exists():
379
+ raise FileNotFoundError(f"BED file not found: {bed}")
380
+ if gtf and not gtf.exists():
381
+ raise FileNotFoundError(f"GTF file not found: {gtf}")
382
+ if sites and not sites.exists():
383
+ raise FileNotFoundError(f"Sites VCF/BCF file not found: {sites}")
384
+ if genome and not genome.exists():
385
+ raise FileNotFoundError(f"Genome accessibility file not found: {genome}")
386
+ if threads < 1:
387
+ raise ValueError("Number of threads must be at least 1.")
388
+
389
+ cmd = ["alfred", "qc", "-o", str(outfile), "-r", str(ref)]
390
+ output_files = [str(outfile)]
391
+
392
+ if bed:
393
+ cmd.extend(["-b", str(bed)])
394
+ if gtf:
395
+ cmd.extend(["-j", str(gtf)])
396
+ if threads > 1:
397
+ cmd.extend(["-p", str(threads)])
398
+ if minmapq != 0:
399
+ cmd.extend(["-m", str(minmapq)])
400
+ if flag != 0:
401
+ cmd.extend(["-f", str(flag)])
402
+ if fflag != 1540:
403
+ cmd.extend(["-F", str(fflag)])
404
+ if contigs:
405
+ cmd.extend(["-c", contigs])
406
+ if feature != "exon":
407
+ cmd.extend(["-e", feature])
408
+ if id_attribute != "gene_id":
409
+ cmd.extend(["-i", id_attribute])
410
+ if stranded != 0:
411
+ cmd.extend(["-a", str(stranded)])
412
+ if antisense:
413
+ cmd.append("-z")
414
+ if uncompressed:
415
+ cmd.append("-u")
416
+ if sites:
417
+ cmd.extend(["-s", str(sites)])
418
+ if genome:
419
+ cmd.extend(["-g", str(genome)])
420
+
421
+ cmd.extend([str(bam) for bam in input_bams])
422
+
423
+ try:
424
+ result = subprocess.run(
425
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
426
+ )
427
+ return {
428
+ "command_executed": " ".join(cmd),
429
+ "stdout": result.stdout,
430
+ "stderr": result.stderr,
431
+ "output_files": output_files,
432
+ }
433
+ except subprocess.CalledProcessError as e:
434
+ return {
435
+ "command_executed": " ".join(cmd),
436
+ "stdout": e.stdout,
437
+ "stderr": e.stderr,
438
+ "error": "Alfred qc failed",
439
+ "return_code": e.returncode,
440
+ }
441
+
442
+
443
+ @mcp.tool()
444
+ def alfred_merge(
445
+ input_qc_files: List[Path], outfile: Path, uncompressed: bool = False
446
+ ):
447
+ """
448
+ Merge multiple alfred QC files.
449
+
450
+ Args:
451
+ input_qc_files: List of input QC JSON files (e.g., *.qc.json.gz).
452
+ outfile: Output file for merged QC metrics (e.g., out.qc.json.gz).
453
+ uncompressed: Write uncompressed output.
454
+ """
455
+ if not input_qc_files:
456
+ raise ValueError("At least one input QC file is required.")
457
+ for qc_file in input_qc_files:
458
+ if not qc_file.exists():
459
+ raise FileNotFoundError(f"Input QC file not found: {qc_file}")
460
+
461
+ cmd = ["alfred", "merge", "-o", str(outfile)]
462
+ output_files = [str(outfile)]
463
+
464
+ if uncompressed:
465
+ cmd.append("-u")
466
+
467
+ cmd.extend([str(f) for f in input_qc_files])
468
+
469
+ try:
470
+ result = subprocess.run(
471
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
472
+ )
473
+ return {
474
+ "command_executed": " ".join(cmd),
475
+ "stdout": result.stdout,
476
+ "stderr": result.stderr,
477
+ "output_files": output_files,
478
+ }
479
+ except subprocess.CalledProcessError as e:
480
+ return {
481
+ "command_executed": " ".join(cmd),
482
+ "stdout": e.stdout,
483
+ "stderr": e.stderr,
484
+ "error": "Alfred merge failed",
485
+ "return_code": e.returncode,
486
+ }
487
+
488
+
489
+ @mcp.tool()
490
+ def alfred_track(
491
+ in_bam: Path,
492
+ ref: Path,
493
+ outfile: Optional[Path] = None,
494
+ bed: Optional[Path] = None,
495
+ threads: int = 1,
496
+ minmapq: int = 0,
497
+ flag: int = 0,
498
+ fflag: int = 1540,
499
+ step: int = 1000,
500
+ window: int = 1000,
501
+ uncompressed: bool = False,
502
+ ):
503
+ """
504
+ Create a bedGraph track from a BAM file.
505
+
506
+ Args:
507
+ in_bam: Input BAM file.
508
+ ref: Reference FASTA file.
509
+ outfile: Output bedGraph file (e.g., track.bedGraph.gz). Defaults to stdout.
510
+ bed: BED file with regions of interest.
511
+ threads: Number of threads to use.
512
+ minmapq: Minimum mapping quality.
513
+ flag: Required SAM flag.
514
+ fflag: Filtering SAM flag.
515
+ step: Step size for coverage computation.
516
+ window: Window size for coverage computation.
517
+ uncompressed: Write uncompressed output.
518
+ """
519
+ if not in_bam.exists():
520
+ raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
521
+ if not ref.exists():
522
+ raise FileNotFoundError(f"Reference FASTA file not found: {ref}")
523
+ if bed and not bed.exists():
524
+ raise FileNotFoundError(f"BED file not found: {bed}")
525
+ if threads < 1:
526
+ raise ValueError("Number of threads must be at least 1.")
527
+ if step <= 0 or window <= 0:
528
+ raise ValueError("Step and window sizes must be positive.")
529
+
530
+ cmd = ["alfred", "track", "-r", str(ref)]
531
+ output_files = []
532
+
533
+ if outfile:
534
+ cmd.extend(["-o", str(outfile)])
535
+ output_files.append(str(outfile))
536
+ if bed:
537
+ cmd.extend(["-b", str(bed)])
538
+ if threads > 1:
539
+ cmd.extend(["-p", str(threads)])
540
+ if minmapq != 0:
541
+ cmd.extend(["-m", str(minmapq)])
542
+ if flag != 0:
543
+ cmd.extend(["-f", str(flag)])
544
+ if fflag != 1540:
545
+ cmd.extend(["-F", str(fflag)])
546
+ if step != 1000:
547
+ cmd.extend(["-s", str(step)])
548
+ if window != 1000:
549
+ cmd.extend(["-w", str(window)])
550
+ if uncompressed:
551
+ cmd.append("-u")
552
+
553
+ cmd.append(str(in_bam))
554
+
555
+ try:
556
+ result = subprocess.run(
557
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
558
+ )
559
+ return {
560
+ "command_executed": " ".join(cmd),
561
+ "stdout": result.stdout,
562
+ "stderr": result.stderr,
563
+ "output_files": output_files,
564
+ }
565
+ except subprocess.CalledProcessError as e:
566
+ return {
567
+ "command_executed": " ".join(cmd),
568
+ "stdout": e.stdout,
569
+ "stderr": e.stderr,
570
+ "error": "Alfred track failed",
571
+ "return_code": e.returncode,
572
+ }
573
+
574
+
575
+ @mcp.tool()
576
+ def alfred_motif(
577
+ ref_fa: Path,
578
+ motif: str,
579
+ outfile: Optional[Path] = None,
580
+ mincount: int = 1,
581
+ uncompressed: bool = False,
582
+ ):
583
+ """
584
+ Search for motifs in a FASTA file.
585
+
586
+ Args:
587
+ ref_fa: Reference FASTA file.
588
+ motif: Motif to search for.
589
+ outfile: Output BED file (e.g., motif.bed.gz). Defaults to stdout.
590
+ mincount: Minimum number of motif repeats.
591
+ uncompressed: Write uncompressed output.
592
+ """
593
+ if not ref_fa.exists():
594
+ raise FileNotFoundError(f"Reference FASTA file not found: {ref_fa}")
595
+ if not motif:
596
+ raise ValueError("Motif string cannot be empty.")
597
+ if mincount < 1:
598
+ raise ValueError("Minimum count must be at least 1.")
599
+
600
+ cmd = ["alfred", "motif", "-m", motif]
601
+ output_files = []
602
+
603
+ if outfile:
604
+ cmd.extend(["-o", str(outfile)])
605
+ output_files.append(str(outfile))
606
+ if mincount != 1:
607
+ cmd.extend(["-c", str(mincount)])
608
+ if uncompressed:
609
+ cmd.append("-u")
610
+
611
+ cmd.append(str(ref_fa))
612
+
613
+ try:
614
+ result = subprocess.run(
615
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
616
+ )
617
+ return {
618
+ "command_executed": " ".join(cmd),
619
+ "stdout": result.stdout,
620
+ "stderr": result.stderr,
621
+ "output_files": output_files,
622
+ }
623
+ except subprocess.CalledProcessError as e:
624
+ return {
625
+ "command_executed": " ".join(cmd),
626
+ "stdout": e.stdout,
627
+ "stderr": e.stderr,
628
+ "error": "Alfred motif failed",
629
+ "return_code": e.returncode,
630
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/alfred_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/alfred_server.py')
11
+ SERVER_NAME = 'biosci_alfred'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/app/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-alfred:
5
+ build: .
6
+ image: mcp-alfred:latest
7
+ container_name: mcp-alfred
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=alfred
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - alfred
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_alfred/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install anansescanpy via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda anansescanpy -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY anansescanpy_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/anansescanpy_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/anansescanpy_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/app/anansescanpy_server.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ import shlex
5
+ import textwrap
6
+
7
+ @mcp.tool()
8
+ def export_scanpy(
9
+ adata_path: str,
10
+ cluster_id: str,
11
+ output_dir: str = "scANANSE",
12
+ rna_type: str = "raw",
13
+ min_cells: int = 50,
14
+ is_multiome: bool = False,
15
+ ):
16
+ """
17
+ Export Scanpy AnnData object to files compatible with ANANSE.
18
+ This function prepares the gene expression and (optionally) accessibility data
19
+ for each cluster defined in the AnnData object.
20
+
21
+ Args:
22
+ adata_path: Path to the input AnnData file (.h5ad).
23
+ cluster_id: The column in adata.obs containing cluster/cell-type labels.
24
+ output_dir: Directory where the exported files will be saved.
25
+ rna_type: Type of RNA data ('raw' or 'recount').
26
+ min_cells: Minimum number of cells per cluster to be included in the export.
27
+ is_multiome: Set to True if the AnnData object contains both RNA and ATAC data.
28
+ """
29
+ # Input validation
30
+ p_adata = Path(adata_path)
31
+ if not p_adata.exists():
32
+ return {"error": f"Input file {adata_path} not found."}
33
+
34
+ if rna_type not in ["raw", "recount"]:
35
+ return {"error": "rna_type must be either 'raw' or 'recount'."}
36
+
37
+ if min_cells < 0:
38
+ return {"error": "min_cells must be a non-negative integer."}
39
+
40
+ # Construct Python script to execute the library function
41
+ # We use a subprocess to ensure we can capture output and handle the environment
42
+ py_script = textwrap.dedent(f"""
43
+ import scanpy as sc
44
+ import anansescanpy as aspy
45
+ import os
46
+
47
+ try:
48
+ adata = sc.read_h5ad('{adata_path}')
49
+ aspy.export_scanpy(
50
+ adata,
51
+ cluster_id='{cluster_id}',
52
+ rna_type='{rna_type}',
53
+ min_cells={min_cells},
54
+ output_dir='{output_dir}',
55
+ is_multiome={is_multiome}
56
+ )
57
+ print("Export successful")
58
+ except Exception as e:
59
+ print(f"Error: {{str(e)}}")
60
+ exit(1)
61
+ """)
62
+
63
+ try:
64
+ result = subprocess.run(
65
+ ["python3", "-c", py_script],
66
+ capture_output=True,
67
+ text=True,
68
+ check=True
69
+ )
70
+ return {
71
+ "command_executed": f"anansescanpy.export_scanpy on {adata_path}",
72
+ "stdout": result.stdout,
73
+ "stderr": result.stderr,
74
+ "output_dir": output_dir
75
+ }
76
+ except subprocess.CalledProcessError as e:
77
+ return {
78
+ "error": "Failed to export Scanpy object",
79
+ "stdout": e.stdout,
80
+ "stderr": e.stderr
81
+ }
82
+
83
+ @mcp.tool()
84
+ def import_scanpy(
85
+ adata_path: str,
86
+ ananse_dir: str,
87
+ cluster_id: str,
88
+ output_path: str,
89
+ ):
90
+ """
91
+ Import ANANSE results (influence scores, GRN) back into a Scanpy AnnData object.
92
+
93
+ Args:
94
+ adata_path: Path to the original AnnData file (.h5ad).
95
+ ananse_dir: Directory containing the ANANSE output files.
96
+ cluster_id: The column in adata.obs that was used for the ANANSE analysis.
97
+ output_path: Path where the updated AnnData file will be saved.
98
+ """
99
+ p_adata = Path(adata_path)
100
+ p_ananse = Path(ananse_dir)
101
+
102
+ if not p_adata.exists():
103
+ return {"error": f"Input AnnData file {adata_path} not found."}
104
+ if not p_ananse.is_dir():
105
+ return {"error": f"ANANSE directory {ananse_dir} not found."}
106
+
107
+ py_script = textwrap.dedent(f"""
108
+ import scanpy as sc
109
+ import anansescanpy as aspy
110
+
111
+ try:
112
+ adata = sc.read_h5ad('{adata_path}')
113
+ adata = aspy.import_scanpy(
114
+ adata,
115
+ cluster_id='{cluster_id}',
116
+ ananse_dir='{ananse_dir}'
117
+ )
118
+ adata.write_h5ad('{output_path}')
119
+ print("Import successful")
120
+ except Exception as e:
121
+ print(f"Error: {{str(e)}}")
122
+ exit(1)
123
+ """)
124
+
125
+ try:
126
+ result = subprocess.run(
127
+ ["python3", "-c", py_script],
128
+ capture_output=True,
129
+ text=True,
130
+ check=True
131
+ )
132
+ return {
133
+ "command_executed": f"anansescanpy.import_scanpy from {ananse_dir}",
134
+ "stdout": result.stdout,
135
+ "stderr": result.stderr,
136
+ "output_file": output_path
137
+ }
138
+ except subprocess.CalledProcessError as e:
139
+ return {
140
+ "error": "Failed to import ANANSE results",
141
+ "stdout": e.stdout,
142
+ "stderr": e.stderr
143
+ }
144
+
145
+ @mcp.tool()
146
+ def anansnake_config(
147
+ adata_path: str,
148
+ cluster_id: str,
149
+ genome: str,
150
+ output_dir: str,
151
+ sample_col: Optional[str] = None,
152
+ atac_column: Optional[str] = None,
153
+ ):
154
+ """
155
+ Generate a configuration file for the anansnake pipeline based on a Scanpy object.
156
+
157
+ Args:
158
+ adata_path: Path to the AnnData file (.h5ad).
159
+ cluster_id: The column in adata.obs containing cluster labels.
160
+ genome: Genome assembly name (e.g., 'hg38', 'mm10').
161
+ output_dir: Directory where the config.yaml and analysis files will be created.
162
+ sample_col: Optional column in adata.obs containing sample identifiers.
163
+ atac_column: Optional column in adata.obs containing ATAC-seq peak data.
164
+ """
165
+ p_adata = Path(adata_path)
166
+ if not p_adata.exists():
167
+ return {"error": f"Input file {adata_path} not found."}
168
+
169
+ sample_arg = f"'{sample_col}'" if sample_col else "None"
170
+ atac_arg = f"'{atac_column}'" if atac_column else "None"
171
+
172
+ py_script = textwrap.dedent(f"""
173
+ import scanpy as sc
174
+ import anansescanpy as aspy
175
+
176
+ try:
177
+ adata = sc.read_h5ad('{adata_path}')
178
+ aspy.anansnake_config(
179
+ adata,
180
+ cluster_id='{cluster_id}',
181
+ genome='{genome}',
182
+ output_dir='{output_dir}',
183
+ sample_col={sample_arg},
184
+ atac_column={atac_arg}
185
+ )
186
+ print("Config generation successful")
187
+ except Exception as e:
188
+ print(f"Error: {{str(e)}}")
189
+ exit(1)
190
+ """)
191
+
192
+ try:
193
+ result = subprocess.run(
194
+ ["python3", "-c", py_script],
195
+ capture_output=True,
196
+ text=True,
197
+ check=True
198
+ )
199
+ return {
200
+ "command_executed": "anansescanpy.anansnake_config",
201
+ "stdout": result.stdout,
202
+ "stderr": result.stderr,
203
+ "output_dir": output_dir
204
+ }
205
+ except subprocess.CalledProcessError as e:
206
+ return {
207
+ "error": "Failed to generate anansnake config",
208
+ "stdout": e.stdout,
209
+ "stderr": e.stderr
210
+ }
211
+
212
+ @mcp.tool()
213
+ def run_anansnake(
214
+ config_file: str,
215
+ snakefile: str,
216
+ cores: int = 12,
217
+ memory_mb: int = 48000,
218
+ use_conda: bool = True,
219
+ conda_frontend: str = "mamba",
220
+ ):
221
+ """
222
+ Execute the anansnake GRN analysis pipeline using Snakemake.
223
+
224
+ Args:
225
+ config_file: Path to the config.yaml file (generated by anansnake_config).
226
+ snakefile: Path to the anansnake Snakefile.
227
+ cores: Number of CPU cores to use.
228
+ memory_mb: Memory resource limit in MB.
229
+ use_conda: Whether to use conda environments for the pipeline steps.
230
+ conda_frontend: The conda frontend to use ('mamba' or 'conda').
231
+ """
232
+ p_config = Path(config_file)
233
+ p_snake = Path(snakefile)
234
+
235
+ if not p_config.exists():
236
+ return {"error": f"Config file {config_file} not found."}
237
+ if not p_snake.exists():
238
+ return {"error": f"Snakefile {snakefile} not found."}
239
+
240
+ cmd = [
241
+ "snakemake",
242
+ "--configfile", str(p_config),
243
+ "--snakefile", str(p_snake),
244
+ "--cores", str(cores),
245
+ "--resources", f"mem_mb={memory_mb}"
246
+ ]
247
+
248
+ if use_conda:
249
+ cmd.append("--use-conda")
250
+ cmd.extend(["--conda-frontend", conda_frontend])
251
+
252
+ try:
253
+ result = subprocess.run(
254
+ cmd,
255
+ capture_output=True,
256
+ text=True,
257
+ check=True
258
+ )
259
+ return {
260
+ "command_executed": " ".join(cmd),
261
+ "stdout": result.stdout,
262
+ "stderr": result.stderr
263
+ }
264
+ except subprocess.CalledProcessError as e:
265
+ return {
266
+ "error": "Snakemake pipeline execution failed",
267
+ "command_executed": " ".join(cmd),
268
+ "stdout": e.stdout,
269
+ "stderr": e.stderr
270
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/app/anansescanpy_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/app/anansescanpy_server.py')
11
+ SERVER_NAME = 'biosci_anansescanpy'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-anansescanpy:
5
+ build: .
6
+ image: mcp-anansescanpy:latest
7
+ container_name: mcp-anansescanpy
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=anansescanpy
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - anansescanpy
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_anansescanpy/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install aria2 via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda aria2 -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY aria2_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/aria2_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/aria2_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/app/aria2_server.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import List, Optional, Union
4
+
5
+ @mcp.tool()
6
+ def aria2_download(
7
+ uris: List[str],
8
+ dir: Optional[str] = None,
9
+ out: Optional[str] = None,
10
+ split: int = 5,
11
+ max_connection_per_server: int = 1,
12
+ min_split_size: str = "20M",
13
+ continue_download: bool = True,
14
+ max_download_limit: str = "0",
15
+ user_agent: Optional[str] = None,
16
+ header: Optional[List[str]] = None,
17
+ all_proxy: Optional[str] = None,
18
+ ):
19
+ """
20
+ Download files from HTTP/HTTPS/FTP/SFTP URIs using aria2c.
21
+
22
+ :param uris: List of URIs to download.
23
+ :param dir: The directory to store the downloaded file.
24
+ :param out: The file name of the downloaded file.
25
+ :param split: Download a file using N connections.
26
+ :param max_connection_per_server: The maximum number of connections to one server for each download.
27
+ :param min_split_size: aria2 does not split less than this size. (e.g., 20M)
28
+ :param continue_download: Continue downloading a partially downloaded file.
29
+ :param max_download_limit: Set max download speed in bytes/sec. 0 means unrestricted. (e.g., 1M, 500K)
30
+ :param user_agent: Set user agent.
31
+ :param header: Append HTTP header.
32
+ :param all_proxy: Use proxy server for all protocols.
33
+ """
34
+ cmd = ["aria2c"]
35
+
36
+ if dir:
37
+ path_dir = Path(dir)
38
+ path_dir.mkdir(parents=True, exist_ok=True)
39
+ cmd.extend(["--dir", str(path_dir)])
40
+
41
+ if out:
42
+ cmd.extend(["--out", out])
43
+
44
+ cmd.extend([
45
+ f"--split={split}",
46
+ f"--max-connection-per-server={max_connection_per_server}",
47
+ f"--min-split-size={min_split_size}",
48
+ f"--continue={str(continue_download).lower()}",
49
+ f"--max-download-limit={max_download_limit}"
50
+ ])
51
+
52
+ if user_agent:
53
+ cmd.extend([f"--user-agent={user_agent}"])
54
+
55
+ if header:
56
+ for h in header:
57
+ cmd.extend([f"--header={h}"])
58
+
59
+ if all_proxy:
60
+ cmd.extend([f"--all-proxy={all_proxy}"])
61
+
62
+ cmd.extend(uris)
63
+
64
+ try:
65
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
66
+ return {
67
+ "command_executed": " ".join(cmd),
68
+ "stdout": result.stdout,
69
+ "stderr": result.stderr,
70
+ "status": "success"
71
+ }
72
+ except subprocess.CalledProcessError as e:
73
+ return {
74
+ "command_executed": " ".join(cmd),
75
+ "stdout": e.stdout,
76
+ "stderr": e.stderr,
77
+ "error": str(e),
78
+ "status": "error"
79
+ }
80
+
81
+ @mcp.tool()
82
+ def aria2_torrent(
83
+ torrent_path: str,
84
+ dir: Optional[str] = None,
85
+ seed_time: int = 0,
86
+ max_upload_limit: str = "0",
87
+ listen_port: str = "6881-6999",
88
+ select_file: Optional[str] = None,
89
+ follow_torrent: str = "true",
90
+ ):
91
+ """
92
+ Download files using a BitTorrent file or Magnet URI.
93
+
94
+ :param torrent_path: Path to .torrent file or a Magnet URI.
95
+ :param dir: The directory to store the downloaded file.
96
+ :param seed_time: Specify seeding time in minutes. 0 means do not seed.
97
+ :param max_upload_limit: Set max upload speed in bytes/sec.
98
+ :param listen_port: Set TCP port number for BitTorrent.
99
+ :param select_file: Index of the file to download (e.g., "1,2,5").
100
+ :param follow_torrent: If 'true' or 'mem', aria2 will download the torrent file if URI is a torrent file.
101
+ """
102
+ cmd = ["aria2c"]
103
+
104
+ if dir:
105
+ path_dir = Path(dir)
106
+ path_dir.mkdir(parents=True, exist_ok=True)
107
+ cmd.extend(["--dir", str(path_dir)])
108
+
109
+ cmd.extend([
110
+ f"--seed-time={seed_time}",
111
+ f"--max-upload-limit={max_upload_limit}",
112
+ f"--listen-port={listen_port}",
113
+ f"--follow-torrent={follow_torrent}"
114
+ ])
115
+
116
+ if select_file:
117
+ cmd.extend([f"--select-file={select_file}"])
118
+
119
+ # Check if it's a local file or a magnet link
120
+ if not torrent_path.startswith("magnet:?"):
121
+ path_torrent = Path(torrent_path)
122
+ if not path_torrent.exists():
123
+ return {"error": f"Torrent file not found: {torrent_path}"}
124
+ cmd.append(str(path_torrent))
125
+ else:
126
+ cmd.append(torrent_path)
127
+
128
+ try:
129
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
130
+ return {
131
+ "command_executed": " ".join(cmd),
132
+ "stdout": result.stdout,
133
+ "stderr": result.stderr,
134
+ "status": "success"
135
+ }
136
+ except subprocess.CalledProcessError as e:
137
+ return {
138
+ "command_executed": " ".join(cmd),
139
+ "stdout": e.stdout,
140
+ "stderr": e.stderr,
141
+ "error": str(e),
142
+ "status": "error"
143
+ }
144
+
145
+ @mcp.tool()
146
+ def aria2_metalink(
147
+ metalink_file: str,
148
+ dir: Optional[str] = None,
149
+ metalink_language: Optional[str] = None,
150
+ metalink_location: Optional[str] = None,
151
+ metalink_os: Optional[str] = None,
152
+ ):
153
+ """
154
+ Download files using a Metalink file.
155
+
156
+ :param metalink_file: Path to the .metalink file.
157
+ :param dir: The directory to store the downloaded file.
158
+ :param metalink_language: The language of the file to download.
159
+ :param metalink_location: The location of the preferred server.
160
+ :param metalink_os: The operating system of the file to download.
161
+ """
162
+ path_meta = Path(metalink_file)
163
+ if not path_meta.exists():
164
+ return {"error": f"Metalink file not found: {metalink_file}"}
165
+
166
+ cmd = ["aria2c"]
167
+
168
+ if dir:
169
+ path_dir = Path(dir)
170
+ path_dir.mkdir(parents=True, exist_ok=True)
171
+ cmd.extend(["--dir", str(path_dir)])
172
+
173
+ if metalink_language:
174
+ cmd.extend([f"--metalink-language={metalink_language}"])
175
+ if metalink_location:
176
+ cmd.extend([f"--metalink-location={metalink_location}"])
177
+ if metalink_os:
178
+ cmd.extend([f"--metalink-os={metalink_os}"])
179
+
180
+ cmd.append(str(path_meta))
181
+
182
+ try:
183
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
184
+ return {
185
+ "command_executed": " ".join(cmd),
186
+ "stdout": result.stdout,
187
+ "stderr": result.stderr,
188
+ "status": "success"
189
+ }
190
+ except subprocess.CalledProcessError as e:
191
+ return {
192
+ "command_executed": " ".join(cmd),
193
+ "stdout": e.stdout,
194
+ "stderr": e.stderr,
195
+ "error": str(e),
196
+ "status": "error"
197
+ }
198
+
199
+ @mcp.tool()
200
+ def aria2_batch_download(
201
+ input_file: str,
202
+ dir: Optional[str] = None,
203
+ force_sequential: bool = False,
204
+ max_concurrent_downloads: int = 5,
205
+ parameterized_uris: bool = False,
206
+ ):
207
+ """
208
+ Download multiple URIs from a text file.
209
+
210
+ :param input_file: Path to the file containing URIs (one per line).
211
+ :param dir: The directory to store the downloaded files.
212
+ :param force_sequential: Fetch URIs in the order they appear in the input file.
213
+ :param max_concurrent_downloads: Set maximum number of parallel downloads.
214
+ :param parameterized_uris: Enable parameterized URI support (e.g. http://{host1,host2}/file).
215
+ """
216
+ path_input = Path(input_file)
217
+ if not path_input.exists():
218
+ return {"error": f"Input file not found: {input_file}"}
219
+
220
+ cmd = ["aria2c", f"--input-file={str(path_input)}"]
221
+
222
+ if dir:
223
+ path_dir = Path(dir)
224
+ path_dir.mkdir(parents=True, exist_ok=True)
225
+ cmd.extend(["--dir", str(path_dir)])
226
+
227
+ cmd.extend([
228
+ f"--force-sequential={str(force_sequential).lower()}",
229
+ f"--max-concurrent-downloads={max_concurrent_downloads}",
230
+ f"--parameterized-uri={str(parameterized_uris).lower()}"
231
+ ])
232
+
233
+ try:
234
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
235
+ return {
236
+ "command_executed": " ".join(cmd),
237
+ "stdout": result.stdout,
238
+ "stderr": result.stderr,
239
+ "status": "success"
240
+ }
241
+ except subprocess.CalledProcessError as e:
242
+ return {
243
+ "command_executed": " ".join(cmd),
244
+ "stdout": e.stdout,
245
+ "stderr": e.stderr,
246
+ "error": str(e),
247
+ "status": "error"
248
+ }
249
+
250
+ @mcp.tool()
251
+ def aria2_rpc_server(
252
+ rpc_listen_port: int = 6800,
253
+ rpc_listen_all: bool = False,
254
+ rpc_secret: Optional[str] = None,
255
+ rpc_allow_origin_all: bool = False,
256
+ rpc_max_request_size: str = "2M",
257
+ daemon: bool = False,
258
+ ):
259
+ """
260
+ Start aria2 in RPC mode to allow remote control via JSON-RPC or XML-RPC.
261
+
262
+ :param rpc_listen_port: Port number for RPC server.
263
+ :param rpc_listen_all: Listen for RPC requests on all network interfaces.
264
+ :param rpc_secret: Set RPC secret authorization token.
265
+ :param rpc_allow_origin_all: Add Access-Control-Allow-Origin: * to HTTP response.
266
+ :param rpc_max_request_size: Set max size of JSON-RPC/XML-RPC request.
267
+ :param daemon: Run as a background process.
268
+ """
269
+ cmd = ["aria2c", "--enable-rpc"]
270
+
271
+ cmd.extend([
272
+ f"--rpc-listen-port={rpc_listen_port}",
273
+ f"--rpc-listen-all={str(rpc_listen_all).lower()}",
274
+ f"--rpc-allow-origin-all={str(rpc_allow_origin_all).lower()}",
275
+ f"--rpc-max-request-size={rpc_max_request_size}",
276
+ f"--daemon={str(daemon).lower()}"
277
+ ])
278
+
279
+ if rpc_secret:
280
+ cmd.append(f"--rpc-secret={rpc_secret}")
281
+
282
+ try:
283
+ # If daemon is True, we don't wait for completion
284
+ if daemon:
285
+ subprocess.Popen(cmd)
286
+ return {
287
+ "command_executed": " ".join(cmd),
288
+ "status": "daemon_started",
289
+ "message": f"aria2 RPC server started in background on port {rpc_listen_port}"
290
+ }
291
+ else:
292
+ # This will block until the server is stopped
293
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
294
+ return {
295
+ "command_executed": " ".join(cmd),
296
+ "stdout": result.stdout,
297
+ "stderr": result.stderr,
298
+ "status": "success"
299
+ }
300
+ except subprocess.CalledProcessError as e:
301
+ return {
302
+ "command_executed": " ".join(cmd),
303
+ "stdout": e.stdout,
304
+ "stderr": e.stderr,
305
+ "error": str(e),
306
+ "status": "error"
307
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/app/aria2_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/app/aria2_server.py')
11
+ SERVER_NAME = 'biosci_aria2'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-aria2:
5
+ build: .
6
+ image: mcp-aria2:latest
7
+ container_name: mcp-aria2
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=aria2
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - aria2
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_aria2/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install bcbio-nextgen via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda bcbio-nextgen -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY app/bcbio-nextgen_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/bcbio-nextgen_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/bcbio-nextgen_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/app/bcbio-nextgen_server.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Dict, Any
2
+ import subprocess
3
+ from pathlib import Path
4
+ import os
5
+
6
+ @mcp.tool()
7
+ def bcbio_nextgen_run(
8
+ config_file: str,
9
+ num_cores: int = 1,
10
+ parallel_type: str = "local",
11
+ scheduler: Optional[str] = None,
12
+ queue: Optional[str] = None,
13
+ resources: Optional[str] = None,
14
+ tag: Optional[str] = None,
15
+ workdir: Optional[str] = None,
16
+ timeout: int = 15,
17
+ retries: int = 0,
18
+ ) -> Dict[str, Any]:
19
+ """
20
+ Run a bcbio-nextgen analysis pipeline using a provided configuration file.
21
+
22
+ Args:
23
+ config_file: Path to the YAML configuration file defining the analysis.
24
+ num_cores: Number of local cores to use for parallel execution.
25
+ parallel_type: Type of parallel execution (local, ipython, cluster, etc.).
26
+ scheduler: Scheduler for cluster execution (e.g., sge, slurm, torque, pbspro, lsf).
27
+ queue: Queue to submit jobs to on a cluster.
28
+ resources: Specific resource requirements for the scheduler (e.g., 'mem=16,vmem=20').
29
+ tag: Optional tag to identify this specific run.
30
+ workdir: Directory to use for processing (defaults to current directory).
31
+ timeout: Time in minutes to wait for ipython cluster startup.
32
+ retries: Number of times to retry failed steps.
33
+ """
34
+ config_path = Path(config_file)
35
+ if not config_path.exists():
36
+ return {"error": f"Configuration file not found: {config_file}"}
37
+
38
+ cmd = ["bcbio_nextgen.py", str(config_path.absolute())]
39
+
40
+ cmd.extend(["-n", str(num_cores)])
41
+ cmd.extend(["-t", parallel_type])
42
+
43
+ if scheduler:
44
+ cmd.extend(["-s", scheduler])
45
+ if queue:
46
+ cmd.extend(["-q", queue])
47
+ if resources:
48
+ cmd.extend(["-r", resources])
49
+ if tag:
50
+ cmd.extend(["--tag", tag])
51
+ if timeout != 15:
52
+ cmd.extend(["--timeout", str(timeout)])
53
+ if retries > 0:
54
+ cmd.extend(["--retries", str(retries)])
55
+
56
+ # Handle working directory
57
+ original_dir = os.getcwd()
58
+ if workdir:
59
+ work_path = Path(workdir)
60
+ if not work_path.exists():
61
+ work_path.mkdir(parents=True, exist_ok=True)
62
+ os.chdir(work_path)
63
+
64
+ try:
65
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
66
+ return {
67
+ "command_executed": " ".join(cmd),
68
+ "stdout": result.stdout,
69
+ "stderr": result.stderr,
70
+ "status": "success"
71
+ }
72
+ except subprocess.CalledProcessError as e:
73
+ return {
74
+ "command_executed": " ".join(cmd),
75
+ "stdout": e.stdout,
76
+ "stderr": e.stderr,
77
+ "error": str(e),
78
+ "status": "failed"
79
+ }
80
+ finally:
81
+ os.chdir(original_dir)
82
+
83
+ @mcp.tool()
84
+ def bcbio_nextgen_template(
85
+ template_name: str,
86
+ metadata_csv: str,
87
+ input_files: List[str],
88
+ out_dir: Optional[str] = None,
89
+ ) -> Dict[str, Any]:
90
+ """
91
+ Create a bcbio-nextgen processing description from a template and input files.
92
+
93
+ Args:
94
+ template_name: Name of the template to use (e.g., 'freebayes-variant', 'gatk-variant').
95
+ metadata_csv: Path to a CSV file containing sample metadata.
96
+ input_files: List of paths to input BAM or FASTQ files.
97
+ out_dir: Directory to write the generated configuration (defaults to current directory).
98
+ """
99
+ metadata_path = Path(metadata_csv)
100
+ if not metadata_path.exists():
101
+ return {"error": f"Metadata file not found: {metadata_csv}"}
102
+
103
+ # Validate input files
104
+ valid_inputs = []
105
+ for f in input_files:
106
+ p = Path(f)
107
+ if p.exists():
108
+ valid_inputs.append(str(p.absolute()))
109
+ else:
110
+ return {"error": f"Input file not found: {f}"}
111
+
112
+ cmd = ["bcbio_nextgen.py", "-w", "template", template_name, str(metadata_path.absolute())]
113
+ cmd.extend(valid_inputs)
114
+
115
+ # Handle output directory
116
+ original_dir = os.getcwd()
117
+ if out_dir:
118
+ out_path = Path(out_dir)
119
+ if not out_path.exists():
120
+ out_path.mkdir(parents=True, exist_ok=True)
121
+ os.chdir(out_path)
122
+
123
+ try:
124
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
125
+ return {
126
+ "command_executed": " ".join(cmd),
127
+ "stdout": result.stdout,
128
+ "stderr": result.stderr,
129
+ "status": "success",
130
+ "info": "Configuration files generated in the output directory."
131
+ }
132
+ except subprocess.CalledProcessError as e:
133
+ return {
134
+ "command_executed": " ".join(cmd),
135
+ "stdout": e.stdout,
136
+ "stderr": e.stderr,
137
+ "error": str(e),
138
+ "status": "failed"
139
+ }
140
+ finally:
141
+ os.chdir(original_dir)
142
+
143
+ @mcp.tool()
144
+ def bcbio_nextgen_upgrade(
145
+ tooldir: Optional[str] = None,
146
+ tools: bool = False,
147
+ data: bool = False,
148
+ genomes: Optional[List[str]] = None,
149
+ aligners: Optional[List[str]] = None,
150
+ cores: int = 1,
151
+ ) -> Dict[str, Any]:
152
+ """
153
+ Upgrade bcbio-nextgen software, third-party tools, or genome data.
154
+
155
+ Args:
156
+ tooldir: Directory where tools are installed.
157
+ tools: If True, upgrade third-party software tools.
158
+ data: If True, upgrade/install genome data.
159
+ genomes: List of genome builds to install/upgrade (e.g., ['hg38', 'mm10']).
160
+ aligners: List of aligners to install data for (e.g., ['bwa', 'bowtie2']).
161
+ cores: Number of cores to use for data downloads and indexing.
162
+ """
163
+ cmd = ["bcbio_nextgen.py", "upgrade"]
164
+
165
+ if tooldir:
166
+ cmd.extend(["--tooldir", tooldir])
167
+ if tools:
168
+ cmd.append("--tools")
169
+ if data:
170
+ cmd.append("--data")
171
+
172
+ if genomes:
173
+ for g in genomes:
174
+ cmd.extend(["--genomes", g])
175
+
176
+ if aligners:
177
+ for a in aligners:
178
+ cmd.extend(["--aligners", a])
179
+
180
+ cmd.extend(["--cores", str(cores)])
181
+
182
+ try:
183
+ # Upgrades can take a long time, but we capture output
184
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
185
+ return {
186
+ "command_executed": " ".join(cmd),
187
+ "stdout": result.stdout,
188
+ "stderr": result.stderr,
189
+ "status": "success"
190
+ }
191
+ except subprocess.CalledProcessError as e:
192
+ return {
193
+ "command_executed": " ".join(cmd),
194
+ "stdout": e.stdout,
195
+ "stderr": e.stderr,
196
+ "error": str(e),
197
+ "status": "failed"
198
+ }
199
+
200
+ @mcp.tool()
201
+ def bcbio_nextgen_install(
202
+ install_path: str,
203
+ tooldir: str,
204
+ genomes: List[str],
205
+ aligners: List[str],
206
+ nodata: bool = False,
207
+ isolate: bool = False,
208
+ ) -> Dict[str, Any]:
209
+ """
210
+ Run the bcbio-nextgen installer script to set up the environment.
211
+
212
+ Args:
213
+ install_path: Path to install bcbio-nextgen data and code.
214
+ tooldir: Path to install third-party software tools.
215
+ genomes: List of genome builds to install (e.g., ['hg38']).
216
+ aligners: List of aligners to prepare (e.g., ['bwa']).
217
+ nodata: If True, do not install genome data.
218
+ isolate: If True, install into an isolated environment.
219
+ """
220
+ # Note: This assumes bcbio_nextgen_install.py is in the PATH or current directory
221
+ # In a real environment, users might need to download it first.
222
+ cmd = ["python", "bcbio_nextgen_install.py", install_path, "--tooldir=" + tooldir]
223
+
224
+ for g in genomes:
225
+ cmd.extend(["--genomes", g])
226
+ for a in aligners:
227
+ cmd.extend(["--aligners", a])
228
+
229
+ if nodata:
230
+ cmd.append("--nodata")
231
+ if isolate:
232
+ cmd.append("--isolate")
233
+
234
+ try:
235
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
236
+ return {
237
+ "command_executed": " ".join(cmd),
238
+ "stdout": result.stdout,
239
+ "stderr": result.stderr,
240
+ "status": "success"
241
+ }
242
+ except subprocess.CalledProcessError as e:
243
+ return {
244
+ "command_executed": " ".join(cmd),
245
+ "stdout": e.stdout,
246
+ "stderr": e.stderr,
247
+ "error": str(e),
248
+ "status": "failed"
249
+ }
250
+
251
+ @mcp.tool()
252
+ def bcbio_nextgen_version() -> Dict[str, Any]:
253
+ """
254
+ Check the installed version of bcbio-nextgen.
255
+ """
256
+ cmd = ["bcbio_nextgen.py", "--version"]
257
+ try:
258
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
259
+ return {
260
+ "command_executed": " ".join(cmd),
261
+ "stdout": result.stdout.strip(),
262
+ "status": "success"
263
+ }
264
+ except subprocess.CalledProcessError as e:
265
+ return {
266
+ "error": str(e),
267
+ "status": "failed"
268
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/app/bcbio-nextgen_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/app/bcbio-nextgen_server.py')
11
+ SERVER_NAME = 'biosci_bcbio_nextgen'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-bcbio-nextgen:
5
+ build: .
6
+ image: mcp-bcbio-nextgen:latest
7
+ container_name: mcp-bcbio-nextgen
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=bcbio-nextgen
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - bcbio-nextgen
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bcbio-nextgen/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install bioconda-utils via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda bioconda-utils -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY app/bioconda-utils_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/bioconda-utils_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/bioconda-utils_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/__pycache__/bioconda-utils_server.cpython-310.pyc ADDED
Binary file (11.4 kB). View file
 
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/bioconda-utils_server.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+
6
+ # @mcp.tool() decorator is assumed to be imported from a library like 'mcp'
7
+ # In this code, it is commented out as per the instructions.
8
+
9
+ # @mcp.tool()
10
+ def bioconductor_skeleton(
11
+ packages: List[str],
12
+ recipes: Optional[Path] = None,
13
+ config: Optional[Path] = None,
14
+ recursive: bool = False,
15
+ skip_existing: bool = False,
16
+ skip_if_in_other_channels: bool = False,
17
+ output_dir: Optional[Path] = None,
18
+ pkg_version: Optional[str] = None,
19
+ bioc_version: Optional[str] = None,
20
+ force: bool = False,
21
+ ):
22
+ """
23
+ Creates skeleton recipes for one or more Bioconductor packages.
24
+
25
+ Args:
26
+ packages: One or more Bioconductor package names to create skeletons for.
27
+ recipes: Path to the recipes folder.
28
+ config: Path to the bioconda-utils configuration file.
29
+ recursive: If True, create recipes for missing dependencies as well.
30
+ skip_existing: If True, skip recipes that already exist.
31
+ skip_if_in_other_channels: If True, skip recipes that exist in other channels.
32
+ output_dir: Directory to write the new recipes to. Defaults to the recipes folder.
33
+ pkg_version: Specific package version to create a skeleton for.
34
+ bioc_version: Specific Bioconductor version to use.
35
+ force: If True, force creation of a recipe even if it already exists.
36
+ """
37
+ if not packages:
38
+ raise ValueError("At least one package name must be provided.")
39
+
40
+ cmd = ["bioconda-utils", "bioconductor-skeleton"]
41
+ if recipes:
42
+ cmd.extend(["--recipes", str(recipes)])
43
+ if config:
44
+ cmd.extend(["--config", str(config)])
45
+ if recursive:
46
+ cmd.append("--recursive")
47
+ if skip_existing:
48
+ cmd.append("--skip-existing")
49
+ if skip_if_in_other_channels:
50
+ cmd.append("--skip-if-in-other-channels")
51
+ if output_dir:
52
+ output_dir.mkdir(parents=True, exist_ok=True)
53
+ cmd.extend(["--output-dir", str(output_dir)])
54
+ if pkg_version:
55
+ cmd.extend(["--pkg-version", pkg_version])
56
+ if bioc_version:
57
+ cmd.extend(["--bioc-version", bioc_version])
58
+ if force:
59
+ cmd.append("--force")
60
+
61
+ cmd.extend(packages)
62
+ command_executed = " ".join(cmd)
63
+
64
+ try:
65
+ result = subprocess.run(
66
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
67
+ )
68
+ output_files = [str(output_dir)] if output_dir else []
69
+ return {
70
+ "command_executed": command_executed,
71
+ "stdout": result.stdout,
72
+ "stderr": result.stderr,
73
+ "output_files": output_files,
74
+ }
75
+ except subprocess.CalledProcessError as e:
76
+ return {
77
+ "command_executed": command_executed,
78
+ "stdout": e.stdout,
79
+ "stderr": e.stderr,
80
+ "error": f"Command failed with exit code {e.returncode}",
81
+ "output_files": [],
82
+ }
83
+
84
+
85
+ # @mcp.tool()
86
+ def cran_skeleton(
87
+ packages: List[str],
88
+ recipes: Optional[Path] = None,
89
+ config: Optional[Path] = None,
90
+ recursive: bool = False,
91
+ skip_existing: bool = False,
92
+ skip_if_in_other_channels: bool = False,
93
+ output_dir: Optional[Path] = None,
94
+ pkg_version: Optional[str] = None,
95
+ force: bool = False,
96
+ ):
97
+ """
98
+ Creates skeleton recipes for one or more CRAN packages.
99
+
100
+ Args:
101
+ packages: One or more CRAN package names to create skeletons for.
102
+ recipes: Path to the recipes folder.
103
+ config: Path to the bioconda-utils configuration file.
104
+ recursive: If True, create recipes for missing dependencies as well.
105
+ skip_existing: If True, skip recipes that already exist.
106
+ skip_if_in_other_channels: If True, skip recipes that exist in other channels.
107
+ output_dir: Directory to write the new recipes to. Defaults to the recipes folder.
108
+ pkg_version: Specific package version to create a skeleton for.
109
+ force: If True, force creation of a recipe even if it already exists.
110
+ """
111
+ if not packages:
112
+ raise ValueError("At least one package name must be provided.")
113
+
114
+ cmd = ["bioconda-utils", "cran-skeleton"]
115
+ if recipes:
116
+ cmd.extend(["--recipes", str(recipes)])
117
+ if config:
118
+ cmd.extend(["--config", str(config)])
119
+ if recursive:
120
+ cmd.append("--recursive")
121
+ if skip_existing:
122
+ cmd.append("--skip-existing")
123
+ if skip_if_in_other_channels:
124
+ cmd.append("--skip-if-in-other-channels")
125
+ if output_dir:
126
+ output_dir.mkdir(parents=True, exist_ok=True)
127
+ cmd.extend(["--output-dir", str(output_dir)])
128
+ if pkg_version:
129
+ cmd.extend(["--pkg-version", pkg_version])
130
+ if force:
131
+ cmd.append("--force")
132
+
133
+ cmd.extend(packages)
134
+ command_executed = " ".join(cmd)
135
+
136
+ try:
137
+ result = subprocess.run(
138
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
139
+ )
140
+ output_files = [str(output_dir)] if output_dir else []
141
+ return {
142
+ "command_executed": command_executed,
143
+ "stdout": result.stdout,
144
+ "stderr": result.stderr,
145
+ "output_files": output_files,
146
+ }
147
+ except subprocess.CalledProcessError as e:
148
+ return {
149
+ "command_executed": command_executed,
150
+ "stdout": e.stdout,
151
+ "stderr": e.stderr,
152
+ "error": f"Command failed with exit code {e.returncode}",
153
+ "output_files": [],
154
+ }
155
+
156
+
157
+ # @mcp.tool()
158
+ def pypi_skeleton(
159
+ packages: List[str],
160
+ recipes: Optional[Path] = None,
161
+ config: Optional[Path] = None,
162
+ recursive: bool = False,
163
+ skip_existing: bool = False,
164
+ skip_if_in_other_channels: bool = False,
165
+ output_dir: Optional[Path] = None,
166
+ pkg_version: Optional[str] = None,
167
+ python_version: Optional[str] = None,
168
+ force: bool = False,
169
+ ):
170
+ """
171
+ Creates skeleton recipes for one or more PyPI packages.
172
+
173
+ Args:
174
+ packages: One or more PyPI package names to create skeletons for.
175
+ recipes: Path to the recipes folder.
176
+ config: Path to the bioconda-utils configuration file.
177
+ recursive: If True, create recipes for missing dependencies as well.
178
+ skip_existing: If True, skip recipes that already exist.
179
+ skip_if_in_other_channels: If True, skip recipes that exist in other channels.
180
+ output_dir: Directory to write the new recipes to. Defaults to the recipes folder.
181
+ pkg_version: Specific package version to create a skeleton for.
182
+ python_version: Python version to use for the skeleton.
183
+ force: If True, force creation of a recipe even if it already exists.
184
+ """
185
+ if not packages:
186
+ raise ValueError("At least one package name must be provided.")
187
+
188
+ cmd = ["bioconda-utils", "pypi-skeleton"]
189
+ if recipes:
190
+ cmd.extend(["--recipes", str(recipes)])
191
+ if config:
192
+ cmd.extend(["--config", str(config)])
193
+ if recursive:
194
+ cmd.append("--recursive")
195
+ if skip_existing:
196
+ cmd.append("--skip-existing")
197
+ if skip_if_in_other_channels:
198
+ cmd.append("--skip-if-in-other-channels")
199
+ if output_dir:
200
+ output_dir.mkdir(parents=True, exist_ok=True)
201
+ cmd.extend(["--output-dir", str(output_dir)])
202
+ if pkg_version:
203
+ cmd.extend(["--pkg-version", pkg_version])
204
+ if python_version:
205
+ cmd.extend(["--python-version", python_version])
206
+ if force:
207
+ cmd.append("--force")
208
+
209
+ cmd.extend(packages)
210
+ command_executed = " ".join(cmd)
211
+
212
+ try:
213
+ result = subprocess.run(
214
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
215
+ )
216
+ output_files = [str(output_dir)] if output_dir else []
217
+ return {
218
+ "command_executed": command_executed,
219
+ "stdout": result.stdout,
220
+ "stderr": result.stderr,
221
+ "output_files": output_files,
222
+ }
223
+ except subprocess.CalledProcessError as e:
224
+ return {
225
+ "command_executed": command_executed,
226
+ "stdout": e.stdout,
227
+ "stderr": e.stderr,
228
+ "error": f"Command failed with exit code {e.returncode}",
229
+ "output_files": [],
230
+ }
231
+
232
+
233
+ # @mcp.tool()
234
+ def lint(
235
+ recipes_to_lint: Optional[List[str]] = None,
236
+ recipes_folder: Optional[Path] = None,
237
+ config: Optional[Path] = None,
238
+ fail_level: str = "error",
239
+ no_fail_on_error: bool = False,
240
+ report_file: Optional[Path] = None,
241
+ exclude: Optional[List[str]] = None,
242
+ ):
243
+ """
244
+ Lints bioconda recipes to check for common errors and style issues.
245
+
246
+ Args:
247
+ recipes_to_lint: Specific recipes to lint. Can be paths, package names, or glob patterns.
248
+ recipes_folder: Path to the top-level recipes folder.
249
+ config: Path to the bioconda-utils configuration file.
250
+ fail_level: The linting level at which to fail ('error', 'warning', 'info').
251
+ no_fail_on_error: If True, do not exit with an error code on linting failures.
252
+ report_file: File to write the linting report to.
253
+ exclude: A list of recipes to exclude from linting.
254
+ """
255
+ fail_level_choices = ["error", "warning", "info"]
256
+ if fail_level not in fail_level_choices:
257
+ raise ValueError(f"fail_level must be one of {fail_level_choices}")
258
+
259
+ cmd = ["bioconda-utils", "lint"]
260
+ if recipes_folder:
261
+ cmd.extend(["--recipes", str(recipes_folder)])
262
+ if config:
263
+ cmd.extend(["--config", str(config)])
264
+ cmd.extend(["--fail-level", fail_level])
265
+ if no_fail_on_error:
266
+ cmd.append("--no-fail-on-error")
267
+ if report_file:
268
+ cmd.extend(["--report-file", str(report_file)])
269
+ if exclude:
270
+ for item in exclude:
271
+ cmd.extend(["--exclude", item])
272
+ if recipes_to_lint:
273
+ cmd.extend(recipes_to_lint)
274
+
275
+ command_executed = " ".join(cmd)
276
+
277
+ try:
278
+ result = subprocess.run(
279
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
280
+ )
281
+ output_files = [str(report_file)] if report_file else []
282
+ return {
283
+ "command_executed": command_executed,
284
+ "stdout": result.stdout,
285
+ "stderr": result.stderr,
286
+ "output_files": output_files,
287
+ }
288
+ except subprocess.CalledProcessError as e:
289
+ return {
290
+ "command_executed": command_executed,
291
+ "stdout": e.stdout,
292
+ "stderr": e.stderr,
293
+ "error": f"Command failed with exit code {e.returncode}",
294
+ "output_files": [],
295
+ }
296
+
297
+
298
+ # @mcp.tool()
299
+ def build(
300
+ recipes_to_build: List[str],
301
+ recipes_folder: Optional[Path] = None,
302
+ config: Optional[Path] = None,
303
+ package_folder: Optional[Path] = None,
304
+ force: bool = False,
305
+ docker: bool = False,
306
+ mulled_test: Optional[bool] = None,
307
+ extra_channels: Optional[List[str]] = None,
308
+ anaconda_upload: bool = False,
309
+ anaconda_token: Optional[str] = None,
310
+ user: Optional[str] = None,
311
+ dry_run: bool = False,
312
+ ):
313
+ """
314
+ Builds one or more bioconda recipes.
315
+
316
+ Args:
317
+ recipes_to_build: Recipes to build. Can be paths, package names, or glob patterns.
318
+ recipes_folder: Path to the top-level recipes folder.
319
+ config: Path to the bioconda-utils configuration file.
320
+ package_folder: Folder to store the built packages.
321
+ force: If True, force the build even if the package already exists.
322
+ docker: If True, build inside a Docker container.
323
+ mulled_test: Set to True to run mulled tests, False to disable them. Default is tool's default.
324
+ extra_channels: Additional channels to use during the build.
325
+ anaconda_upload: If True, upload the built package to anaconda.org.
326
+ anaconda_token: Anaconda token for uploading.
327
+ user: Anaconda user/organization to upload to.
328
+ dry_run: If True, show what would be done without executing.
329
+ """
330
+ if not recipes_to_build:
331
+ raise ValueError("At least one recipe must be provided to build.")
332
+
333
+ cmd = ["bioconda-utils", "build"]
334
+ if recipes_folder:
335
+ cmd.extend(["--recipes", str(recipes_folder)])
336
+ if config:
337
+ cmd.extend(["--config", str(config)])
338
+ if package_folder:
339
+ package_folder.mkdir(parents=True, exist_ok=True)
340
+ cmd.extend(["--package-folder", str(package_folder)])
341
+ if force:
342
+ cmd.append("--force")
343
+ if docker:
344
+ cmd.append("--docker")
345
+ if mulled_test is True:
346
+ cmd.append("--mulled-test")
347
+ elif mulled_test is False:
348
+ cmd.append("--no-mulled-test")
349
+ if extra_channels:
350
+ for channel in extra_channels:
351
+ cmd.extend(["-c", channel])
352
+ if not anaconda_upload:
353
+ cmd.append("--no-anaconda-upload")
354
+ if anaconda_token:
355
+ cmd.extend(["--anaconda-token", anaconda_token])
356
+ if user:
357
+ cmd.extend(["--user", user])
358
+ if dry_run:
359
+ cmd.append("--dry-run")
360
+
361
+ cmd.extend(recipes_to_build)
362
+ command_executed = " ".join(cmd)
363
+
364
+ try:
365
+ result = subprocess.run(
366
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
367
+ )
368
+ output_files = [str(package_folder)] if package_folder else []
369
+ return {
370
+ "command_executed": command_executed,
371
+ "stdout": result.stdout,
372
+ "stderr": result.stderr,
373
+ "output_files": output_files,
374
+ }
375
+ except subprocess.CalledProcessError as e:
376
+ return {
377
+ "command_executed": command_executed,
378
+ "stdout": e.stdout,
379
+ "stderr": e.stderr,
380
+ "error": f"Command failed with exit code {e.returncode}",
381
+ "output_files": [],
382
+ }
383
+
384
+
385
+ # @mcp.tool()
386
+ def dag(
387
+ recipes_for_dag: Optional[List[str]] = None,
388
+ recipes_folder: Optional[Path] = None,
389
+ config: Optional[Path] = None,
390
+ file_out: Optional[Path] = None,
391
+ format: str = "gml",
392
+ ):
393
+ """
394
+ Generates a dependency graph (DAG) for recipes.
395
+
396
+ Args:
397
+ recipes_for_dag: Recipes to include in the DAG. If None, all recipes are considered.
398
+ recipes_folder: Path to the top-level recipes folder.
399
+ config: Path to the bioconda-utils configuration file.
400
+ file_out: Path to write the output DAG file.
401
+ format: The output format for the DAG ('gml', 'dot', 'pdf').
402
+ """
403
+ format_choices = ["gml", "dot", "pdf"]
404
+ if format not in format_choices:
405
+ raise ValueError(f"format must be one of {format_choices}")
406
+
407
+ cmd = ["bioconda-utils", "dag"]
408
+ if recipes_folder:
409
+ cmd.extend(["--recipes", str(recipes_folder)])
410
+ if config:
411
+ cmd.extend(["--config", str(config)])
412
+ if file_out:
413
+ cmd.extend(["--file-out", str(file_out)])
414
+ cmd.extend(["--format", format])
415
+ if recipes_for_dag:
416
+ cmd.extend(recipes_for_dag)
417
+
418
+ command_executed = " ".join(cmd)
419
+
420
+ try:
421
+ result = subprocess.run(
422
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
423
+ )
424
+ output_files = [str(file_out)] if file_out else []
425
+ return {
426
+ "command_executed": command_executed,
427
+ "stdout": result.stdout,
428
+ "stderr": result.stderr,
429
+ "output_files": output_files,
430
+ }
431
+ except subprocess.CalledProcessError as e:
432
+ return {
433
+ "command_executed": command_executed,
434
+ "stdout": e.stdout,
435
+ "stderr": e.stderr,
436
+ "error": f"Command failed with exit code {e.returncode}",
437
+ "output_files": [],
438
+ }
439
+
440
+
441
+ # @mcp.tool()
442
+ def update_pinning(
443
+ recipes_folder: Optional[Path] = None,
444
+ config: Optional[Path] = None,
445
+ packages: Optional[str] = None,
446
+ no_pr: bool = False,
447
+ dry_run: bool = False,
448
+ ):
449
+ """
450
+ Updates pinning in recipes based on the global pinning file.
451
+
452
+ Args:
453
+ recipes_folder: Path to the top-level recipes folder.
454
+ config: Path to the bioconda-utils configuration file.
455
+ packages: Comma-separated string of package names to update pinning for.
456
+ no_pr: If True, do not create a pull request with the changes.
457
+ dry_run: If True, show what would be done without executing.
458
+ """
459
+ cmd = ["bioconda-utils", "update-pinning"]
460
+ if recipes_folder:
461
+ cmd.extend(["--recipes", str(recipes_folder)])
462
+ if config:
463
+ cmd.extend(["--config", str(config)])
464
+ if packages:
465
+ cmd.extend(["--packages", packages])
466
+ if no_pr:
467
+ cmd.append("--no-pr")
468
+ if dry_run:
469
+ cmd.append("--dry-run")
470
+
471
+ command_executed = " ".join(cmd)
472
+
473
+ try:
474
+ result = subprocess.run(
475
+ cmd, check=True, capture_output=True, text=True, encoding="utf-8"
476
+ )
477
+ return {
478
+ "command_executed": command_executed,
479
+ "stdout": result.stdout,
480
+ "stderr": result.stderr,
481
+ "output_files": [],
482
+ }
483
+ except subprocess.CalledProcessError as e:
484
+ return {
485
+ "command_executed": command_executed,
486
+ "stdout": e.stdout,
487
+ "stderr": e.stderr,
488
+ "error": f"Command failed with exit code {e.returncode}",
489
+ "output_files": [],
490
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/bioconda-utils_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/bioconda-utils_server.py')
11
+ SERVER_NAME = 'biosci_bioconda_utils'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/app/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-bioconda-utils:
5
+ build: .
6
+ image: mcp-bioconda-utils:latest
7
+ container_name: mcp-bioconda-utils
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=bioconda-utils
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - bioconda-utils
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconda-utils/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install bioconductor-banksy via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda bioconductor-banksy -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY app/bioconductor-banksy_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/bioconductor-banksy_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/bioconductor-banksy_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/bioconductor-banksy_server.py ADDED
@@ -0,0 +1,659 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import tempfile
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional, List, Dict, Any
6
+
7
+ # Note: The @mcp.tool() decorator is assumed to be available in the environment
8
+ # where this code will be run, as per the instructions "NO NEED to import mcp".
9
+
10
+ @mcp.tool()
11
+ def banksy_init_object(
12
+ input_seurat_rdata_path: Path,
13
+ output_banksy_rdata_path: Path,
14
+ assay: str = "Spatial",
15
+ verbose: bool = True,
16
+ ) -> Dict[str, Any]:
17
+ """
18
+ Initializes a BanksyObject from an existing Seurat object.
19
+
20
+ This tool takes an RData file containing a Seurat object, converts it
21
+ into a BanksyObject, and saves the new BanksyObject to an RData file.
22
+ This is often the first step before running the BANKSY algorithm.
23
+
24
+ Requires R and the 'banksy' and 'Seurat' R packages to be installed
25
+ and accessible in the environment.
26
+
27
+ Args:
28
+ input_seurat_rdata_path: Path to the input .RData file containing a Seurat object.
29
+ output_banksy_rdata_path: Path where the new BanksyObject will be saved as an .RData file.
30
+ assay: Name of the assay in the Seurat object to use for spatial data.
31
+ verbose: If TRUE, print messages during execution.
32
+
33
+ Returns:
34
+ A dictionary containing execution details:
35
+ - command_executed: The Rscript command and generated R script path.
36
+ - stdout: Standard output from the Rscript execution.
37
+ - stderr: Standard error from the Rscript execution.
38
+ - output_files: A list of paths to generated output files.
39
+ """
40
+ # 1. Input validation
41
+ if not input_seurat_rdata_path.exists():
42
+ raise FileNotFoundError(f"Input Seurat RData file not found: {input_seurat_rdata_path}")
43
+ if not input_seurat_rdata_path.is_file():
44
+ raise ValueError(f"Input Seurat RData path is not a file: {input_seurat_rdata_path}")
45
+ if output_banksy_rdata_path.suffix.lower() != ".rdata":
46
+ raise ValueError(f"Output RData file must have a .RData extension: {output_banksy_rdata_path}")
47
+ if not assay:
48
+ raise ValueError("Assay name cannot be empty.")
49
+
50
+ # Ensure output directory exists
51
+ output_banksy_rdata_path.parent.mkdir(parents=True, exist_ok=True)
52
+
53
+ # 2. Generate R script content
54
+ r_script_content = f"""
55
+ # Load required packages
56
+ library(Seurat)
57
+ library(banksy)
58
+
59
+ # Define input and output paths
60
+ input_obj_path <- "{input_seurat_rdata_path.as_posix()}"
61
+ output_obj_path <- "{output_banksy_rdata_path.as_posix()}"
62
+
63
+ # Check if input file exists
64
+ if (!file.exists(input_obj_path)) {{
65
+ stop(paste("Input RData file not found:", input_obj_path))
66
+ }}
67
+
68
+ # Load the object from RData. This approach handles cases where the object
69
+ # name inside the RData file is not known beforehand.
70
+ loaded_env <- new.env()
71
+ load(input_obj_path, envir = loaded_env)
72
+
73
+ seurat_obj <- NULL
74
+ for (var_name in ls(loaded_env)) {{
75
+ candidate <- get(var_name, envir = loaded_env)
76
+ if (inherits(candidate, "Seurat")) {{
77
+ seurat_obj <- candidate
78
+ break
79
+ }}
80
+ }}
81
+
82
+ if (is.null(seurat_obj)) {{
83
+ stop("No Seurat object found in the input RData file.")
84
+ }}
85
+
86
+ message("Initializing BanksyObject from Seurat object with parameters:")
87
+ message(paste(" assay:", "{assay}"))
88
+ message(paste(" verbose:", {str(verbose).upper()}))
89
+
90
+ # Create BanksyObject
91
+ banksy_obj <- BanksyObject(
92
+ seurat_obj,
93
+ assay = "{assay}",
94
+ verbose = {str(verbose).upper()}
95
+ )
96
+
97
+ # Save the new BanksyObject. Renaming to 'obj' for consistency with other banksy tools.
98
+ obj <- banksy_obj
99
+ save(obj, file = output_obj_path)
100
+
101
+ message(paste("BanksyObject saved to:", output_obj_path))
102
+ """
103
+
104
+ temp_r_script_path: Optional[Path] = None
105
+ try:
106
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script:
107
+ temp_r_script.write(r_script_content)
108
+ temp_r_script_path = Path(temp_r_script.name)
109
+
110
+ command = ["Rscript", str(temp_r_script_path)]
111
+
112
+ # 3. Subprocess execution
113
+ process = subprocess.run(
114
+ command,
115
+ check=True,
116
+ capture_output=True,
117
+ text=True,
118
+ env=os.environ # Pass current environment to Rscript
119
+ )
120
+
121
+ stdout = process.stdout
122
+ stderr = process.stderr
123
+
124
+ # 4. Error handling: Check for R-specific errors in stderr
125
+ if "Error" in stderr or "stop(" in stderr:
126
+ raise RuntimeError(f"R script execution failed. Stderr: {stderr}")
127
+
128
+ if not output_banksy_rdata_path.exists():
129
+ raise RuntimeError(f"Output file was not created by R script: {output_banksy_rdata_path}")
130
+
131
+ return {
132
+ "command_executed": " ".join(command),
133
+ "stdout": stdout,
134
+ "stderr": stderr,
135
+ "output_files": [str(output_banksy_rdata_path)],
136
+ }
137
+
138
+ except FileNotFoundError:
139
+ raise RuntimeError("Rscript command not found. Is R installed and in your PATH?")
140
+ except subprocess.CalledProcessError as e:
141
+ raise RuntimeError(
142
+ f"Rscript execution failed with exit code {e.returncode}.\n"
143
+ f"Command: {' '.join(e.cmd)}\n"
144
+ f"Stdout: {e.stdout}\n"
145
+ f"Stderr: {e.stderr}"
146
+ )
147
+ finally:
148
+ # Clean up temporary R script
149
+ if temp_r_script_path and temp_r_script_path.exists():
150
+ os.remove(temp_r_script_path)
151
+
152
+
153
+ @mcp.tool()
154
+ def banksy_run_banksy(
155
+ input_rdata_path: Path,
156
+ output_rdata_path: Path,
157
+ k_neighbours: int = 10,
158
+ lambda_param: float = 0.1, # Renamed from 'lambda' to avoid Python keyword conflict
159
+ resolution: float = 0.8,
160
+ n_components: int = 2,
161
+ n_cores: int = 1,
162
+ verbose: bool = True,
163
+ seed: int = 123,
164
+ ) -> Dict[str, Any]:
165
+ """
166
+ Runs the core BANKSY algorithm on a Seurat or BanksyObject.
167
+
168
+ This tool takes an RData file containing a Seurat or BanksyObject,
169
+ applies the BANKSY algorithm for spatial transcriptomics analysis,
170
+ and saves the updated object to a new RData file.
171
+
172
+ Requires R and the 'banksy', 'Seurat', and 'future' R packages to be installed
173
+ and accessible in the environment.
174
+
175
+ Args:
176
+ input_rdata_path: Path to the input .RData file containing a Seurat or BanksyObject.
177
+ output_rdata_path: Path where the updated Seurat/BanksyObject will be saved as an .RData file.
178
+ k_neighbours: Number of neighbours for spatial graph construction.
179
+ lambda_param: Weight of spatial information (lambda parameter).
180
+ resolution: Resolution parameter for clustering.
181
+ n_components: Number of components for dimensionality reduction (e.g., UMAP/tSNE).
182
+ n_cores: Number of cores to use for parallel processing.
183
+ verbose: If TRUE, print messages during execution.
184
+ seed: Random seed for reproducibility.
185
+
186
+ Returns:
187
+ A dictionary containing execution details:
188
+ - command_executed: The Rscript command and generated R script path.
189
+ - stdout: Standard output from the Rscript execution.
190
+ - stderr: Standard error from the Rscript execution.
191
+ - output_files: A list of paths to generated output files.
192
+ """
193
+ # 1. Input validation
194
+ if not input_rdata_path.exists():
195
+ raise FileNotFoundError(f"Input RData file not found: {input_rdata_path}")
196
+ if not input_rdata_path.is_file():
197
+ raise ValueError(f"Input RData path is not a file: {input_rdata_path}")
198
+ if output_rdata_path.suffix.lower() != ".rdata":
199
+ raise ValueError(f"Output RData file must have a .RData extension: {output_rdata_path}")
200
+ if k_neighbours <= 0:
201
+ raise ValueError("k_neighbours must be a positive integer.")
202
+ if not (0 <= lambda_param <= 1):
203
+ raise ValueError("lambda_param must be between 0 and 1.")
204
+ if resolution <= 0:
205
+ raise ValueError("resolution must be a positive float.")
206
+ if n_components <= 0:
207
+ raise ValueError("n_components must be a positive integer.")
208
+ if n_cores <= 0:
209
+ raise ValueError("n_cores must be a positive integer.")
210
+
211
+ # Ensure output directory exists
212
+ output_rdata_path.parent.mkdir(parents=True, exist_ok=True)
213
+
214
+ # 2. Generate R script content
215
+ r_script_content = f"""
216
+ # Load required packages
217
+ library(Seurat)
218
+ library(banksy)
219
+ library(future) # For parallel processing
220
+
221
+ # Set up parallel processing if n_cores > 1
222
+ if ({n_cores} > 1) {{
223
+ plan("multisession", workers = {n_cores})
224
+ }} else {{
225
+ plan("sequential")
226
+ }}
227
+
228
+ # Set random seed for reproducibility
229
+ set.seed({seed})
230
+
231
+ # Define input and output paths
232
+ input_obj_path <- "{input_rdata_path.as_posix()}"
233
+ output_obj_path <- "{output_rdata_path.as_posix()}"
234
+
235
+ # Check if input file exists
236
+ if (!file.exists(input_obj_path)) {{
237
+ stop(paste("Input RData file not found:", input_obj_path))
238
+ }}
239
+
240
+ # Load the object
241
+ loaded_env <- new.env()
242
+ load(input_obj_path, envir = loaded_env)
243
+
244
+ obj <- NULL
245
+ for (var_name in ls(loaded_env)) {{
246
+ candidate <- get(var_name, envir = loaded_env)
247
+ if (inherits(candidate, "Seurat") || inherits(candidate, "BanksyObject")) {{
248
+ obj <- candidate
249
+ break
250
+ }}
251
+ }}
252
+
253
+ if (is.null(obj)) {{
254
+ stop("No Seurat or BanksyObject found in the input RData file.")
255
+ }}
256
+
257
+ message("Running banksy with parameters:")
258
+ message(paste(" k_neighbours:", {k_neighbours}))
259
+ message(paste(" lambda:", {lambda_param}))
260
+ message(paste(" resolution:", {resolution}))
261
+ message(paste(" n_components:", {n_components}))
262
+ message(paste(" n_cores:", {n_cores}))
263
+ message(paste(" verbose:", {str(verbose).upper()}))
264
+ message(paste(" seed:", {seed}))
265
+
266
+ # Run BANKSY
267
+ obj <- runBanksy(
268
+ object = obj,
269
+ k_neighbours = {k_neighbours},
270
+ lambda = {lambda_param},
271
+ resolution = {resolution},
272
+ n_components = {n_components},
273
+ verbose = {str(verbose).upper()}
274
+ )
275
+
276
+ # Save the updated object
277
+ save(obj, file = output_obj_path)
278
+
279
+ message(paste("Updated object saved to:", output_obj_path))
280
+ """
281
+
282
+ temp_r_script_path: Optional[Path] = None
283
+ try:
284
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script:
285
+ temp_r_script.write(r_script_content)
286
+ temp_r_script_path = Path(temp_r_script.name)
287
+
288
+ command = ["Rscript", str(temp_r_script_path)]
289
+
290
+ # 3. Subprocess execution
291
+ process = subprocess.run(
292
+ command,
293
+ check=True,
294
+ capture_output=True,
295
+ text=True,
296
+ env=os.environ
297
+ )
298
+
299
+ stdout = process.stdout
300
+ stderr = process.stderr
301
+
302
+ # 4. Error handling: Check for R-specific errors in stderr
303
+ if "Error" in stderr or "stop(" in stderr:
304
+ raise RuntimeError(f"R script execution failed. Stderr: {stderr}")
305
+
306
+ if not output_rdata_path.exists():
307
+ raise RuntimeError(f"Output file was not created by R script: {output_rdata_path}")
308
+
309
+ return {
310
+ "command_executed": " ".join(command),
311
+ "stdout": stdout,
312
+ "stderr": stderr,
313
+ "output_files": [str(output_rdata_path)],
314
+ }
315
+
316
+ except FileNotFoundError:
317
+ raise RuntimeError("Rscript command not found. Is R installed and in your PATH?")
318
+ except subprocess.CalledProcessError as e:
319
+ raise RuntimeError(
320
+ f"Rscript execution failed with exit code {e.returncode}.\n"
321
+ f"Command: {' '.join(e.cmd)}\n"
322
+ f"Stdout: {e.stdout}\n"
323
+ f"Stderr: {e.stderr}"
324
+ )
325
+ finally:
326
+ # Clean up temporary R script
327
+ if temp_r_script_path and temp_r_script_path.exists():
328
+ os.remove(temp_r_script_path)
329
+
330
+
331
+ @mcp.tool()
332
+ def banksy_cluster_banksy(
333
+ input_rdata_path: Path,
334
+ output_rdata_path: Path,
335
+ resolution: float = 0.8,
336
+ method: str = "leiden",
337
+ verbose: bool = True,
338
+ seed: int = 123,
339
+ ) -> Dict[str, Any]:
340
+ """
341
+ Performs clustering on a BanksyObject or Seurat object after BANKSY analysis.
342
+
343
+ This tool takes an RData file containing a Seurat or BanksyObject (typically
344
+ after `runBanksy`), performs clustering using the specified method and resolution,
345
+ and saves the updated object to a new RData file.
346
+
347
+ Requires R and the 'banksy' and 'Seurat' R packages to be installed
348
+ and accessible in the environment.
349
+
350
+ Args:
351
+ input_rdata_path: Path to the input .RData file containing a Seurat or BanksyObject.
352
+ output_rdata_path: Path where the updated Seurat/BanksyObject will be saved as an .RData file.
353
+ resolution: Resolution parameter for clustering.
354
+ method: Clustering method to use (e.g., "leiden", "louvain").
355
+ verbose: If TRUE, print messages during execution.
356
+ seed: Random seed for reproducibility.
357
+
358
+ Returns:
359
+ A dictionary containing execution details:
360
+ - command_executed: The Rscript command and generated R script path.
361
+ - stdout: Standard output from the Rscript execution.
362
+ - stderr: Standard error from the Rscript execution.
363
+ - output_files: A list of paths to generated output files.
364
+ """
365
+ # 1. Input validation
366
+ if not input_rdata_path.exists():
367
+ raise FileNotFoundError(f"Input RData file not found: {input_rdata_path}")
368
+ if not input_rdata_path.is_file():
369
+ raise ValueError(f"Input RData path is not a file: {input_rdata_path}")
370
+ if output_rdata_path.suffix.lower() != ".rdata":
371
+ raise ValueError(f"Output RData file must have a .RData extension: {output_rdata_path}")
372
+ if resolution <= 0:
373
+ raise ValueError("resolution must be a positive float.")
374
+ if method not in ["leiden", "louvain"]:
375
+ raise ValueError(f"Unsupported clustering method: {method}. Choose from 'leiden', 'louvain'.")
376
+
377
+ # Ensure output directory exists
378
+ output_rdata_path.parent.mkdir(parents=True, exist_ok=True)
379
+
380
+ # 2. Generate R script content
381
+ r_script_content = f"""
382
+ # Load required packages
383
+ library(Seurat)
384
+ library(banksy)
385
+
386
+ # Set random seed for reproducibility
387
+ set.seed({seed})
388
+
389
+ # Define input and output paths
390
+ input_obj_path <- "{input_rdata_path.as_posix()}"
391
+ output_obj_path <- "{output_rdata_path.as_posix()}"
392
+
393
+ # Check if input file exists
394
+ if (!file.exists(input_obj_path)) {{
395
+ stop(paste("Input RData file not found:", input_obj_path))
396
+ }}
397
+
398
+ # Load the object
399
+ loaded_env <- new.env()
400
+ load(input_obj_path, envir = loaded_env)
401
+
402
+ obj <- NULL
403
+ for (var_name in ls(loaded_env)) {{
404
+ candidate <- get(var_name, envir = loaded_env)
405
+ if (inherits(candidate, "Seurat") || inherits(candidate, "BanksyObject")) {{
406
+ obj <- candidate
407
+ break
408
+ }}
409
+ }}
410
+
411
+ if (is.null(obj)) {{
412
+ stop("No Seurat or BanksyObject found in the input RData file.")
413
+ }}
414
+
415
+ message("Clustering banksy object with parameters:")
416
+ message(paste(" resolution:", {resolution}))
417
+ message(paste(" method:", "{method}"))
418
+ message(paste(" verbose:", {str(verbose).upper()}))
419
+ message(paste(" seed:", {seed}))
420
+
421
+ # Cluster BANKSY object
422
+ obj <- clusterBanksy(
423
+ object = obj,
424
+ resolution = {resolution},
425
+ method = "{method}",
426
+ verbose = {str(verbose).upper()}
427
+ )
428
+
429
+ # Save the updated object
430
+ save(obj, file = output_obj_path)
431
+
432
+ message(paste("Updated object saved to:", output_obj_path))
433
+ """
434
+
435
+ temp_r_script_path: Optional[Path] = None
436
+ try:
437
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script:
438
+ temp_r_script.write(r_script_content)
439
+ temp_r_script_path = Path(temp_r_script.name)
440
+
441
+ command = ["Rscript", str(temp_r_script_path)]
442
+
443
+ # 3. Subprocess execution
444
+ process = subprocess.run(
445
+ command,
446
+ check=True,
447
+ capture_output=True,
448
+ text=True,
449
+ env=os.environ
450
+ )
451
+
452
+ stdout = process.stdout
453
+ stderr = process.stderr
454
+
455
+ # 4. Error handling: Check for R-specific errors in stderr
456
+ if "Error" in stderr or "stop(" in stderr:
457
+ raise RuntimeError(f"R script execution failed. Stderr: {stderr}")
458
+
459
+ if not output_rdata_path.exists():
460
+ raise RuntimeError(f"Output file was not created by R script: {output_rdata_path}")
461
+
462
+ return {
463
+ "command_executed": " ".join(command),
464
+ "stdout": stdout,
465
+ "stderr": stderr,
466
+ "output_files": [str(output_rdata_path)],
467
+ }
468
+
469
+ except FileNotFoundError:
470
+ raise RuntimeError("Rscript command not found. Is R installed and in your PATH?")
471
+ except subprocess.CalledProcessError as e:
472
+ raise RuntimeError(
473
+ f"Rscript execution failed with exit code {e.returncode}.\n"
474
+ f"Command: {' '.join(e.cmd)}\n"
475
+ f"Stdout: {e.stdout}\n"
476
+ f"Stderr: {e.stderr}"
477
+ )
478
+ finally:
479
+ # Clean up temporary R script
480
+ if temp_r_script_path and temp_r_script_path.exists():
481
+ os.remove(temp_r_script_path)
482
+
483
+
484
+ @mcp.tool()
485
+ def banksy_spatial_dim_plot(
486
+ input_rdata_path: Path,
487
+ output_plot_path: Path,
488
+ reduction: str = "banksy",
489
+ group_by: str = "banksy_clusters",
490
+ label: bool = True,
491
+ pt_size: float = 1.5,
492
+ verbose: bool = True,
493
+ width: float = 7.0,
494
+ height: float = 7.0,
495
+ units: str = "in",
496
+ dpi: int = 300,
497
+ ) -> Dict[str, Any]:
498
+ """
499
+ Generates a spatial dimensionality plot for a BanksyObject.
500
+
501
+ This tool takes an RData file containing a BanksyObject (typically after
502
+ `runBanksy` and `clusterBanksy`), generates a spatial plot, and saves it
503
+ to an image file (e.g., PNG, PDF).
504
+
505
+ Requires R and the 'banksy', 'Seurat', and 'ggplot2' R packages to be installed
506
+ and accessible in the environment.
507
+
508
+ Args:
509
+ input_rdata_path: Path to the input .RData file containing a BanksyObject.
510
+ output_plot_path: Path where the plot will be saved (e.g., .png, .pdf, .jpeg).
511
+ reduction: Dimensionality reduction to use for plotting (e.g., "banksy", "umap").
512
+ group_by: Feature to group cells by for coloring (e.g., "banksy_clusters").
513
+ label: If TRUE, label clusters on the plot.
514
+ pt_size: Size of the points in the plot.
515
+ verbose: If TRUE, print messages during execution.
516
+ width: Width of the output plot.
517
+ height: Height of the output plot.
518
+ units: Units for width and height ("in", "cm", "mm").
519
+ dpi: Resolution for raster plots (e.g., PNG, JPEG).
520
+
521
+ Returns:
522
+ A dictionary containing execution details:
523
+ - command_executed: The Rscript command and generated R script path.
524
+ - stdout: Standard output from the Rscript execution.
525
+ - stderr: Standard error from the Rscript execution.
526
+ - output_files: A list of paths to generated output files.
527
+ """
528
+ # 1. Input validation
529
+ if not input_rdata_path.exists():
530
+ raise FileNotFoundError(f"Input RData file not found: {input_rdata_path}")
531
+ if not input_rdata_path.is_file():
532
+ raise ValueError(f"Input RData path is not a file: {input_rdata_path}")
533
+
534
+ valid_plot_suffixes = [".png", ".pdf", ".jpeg", ".jpg", ".tiff", ".bmp"]
535
+ if output_plot_path.suffix.lower() not in valid_plot_suffixes:
536
+ raise ValueError(f"Output plot file must have one of the following extensions: {', '.join(valid_plot_suffixes)}")
537
+
538
+ if pt_size <= 0:
539
+ raise ValueError("pt_size must be a positive float.")
540
+ if width <= 0 or height <= 0:
541
+ raise ValueError("Width and height must be positive floats.")
542
+ if units not in ["in", "cm", "mm"]:
543
+ raise ValueError(f"Invalid units: {units}. Choose from 'in', 'cm', 'mm'.")
544
+ if dpi <= 0:
545
+ raise ValueError("DPI must be a positive integer.")
546
+
547
+ # Ensure output directory exists
548
+ output_plot_path.parent.mkdir(parents=True, exist_ok=True)
549
+
550
+ # 2. Generate R script content
551
+ r_script_content = f"""
552
+ # Load required packages
553
+ library(Seurat)
554
+ library(banksy)
555
+ library(ggplot2) # For saving plots
556
+
557
+ # Define input and output paths
558
+ input_obj_path <- "{input_rdata_path.as_posix()}"
559
+ output_plot_path <- "{output_plot_path.as_posix()}"
560
+
561
+ # Check if input file exists
562
+ if (!file.exists(input_obj_path)) {{
563
+ stop(paste("Input RData file not found:", input_obj_path))
564
+ }}
565
+
566
+ # Load the object
567
+ loaded_env <- new.env()
568
+ load(input_obj_path, envir = loaded_env)
569
+
570
+ obj <- NULL
571
+ for (var_name in ls(loaded_env)) {{
572
+ candidate <- get(var_name, envir = loaded_env)
573
+ if (inherits(candidate, "Seurat") || inherits(candidate, "BanksyObject")) {{
574
+ obj <- candidate
575
+ break
576
+ }}
577
+ }}
578
+
579
+ if (is.null(obj)) {{
580
+ stop("No Seurat or BanksyObject found in the input RData file.")
581
+ }}
582
+
583
+ message("Generating spatial dimensionality plot with parameters:")
584
+ message(paste(" reduction:", "{reduction}"))
585
+ message(paste(" group_by:", "{group_by}"))
586
+ message(paste(" label:", {str(label).upper()}))
587
+ message(paste(" pt_size:", {pt_size}))
588
+ message(paste(" verbose:", {str(verbose).upper()}))
589
+
590
+ # Generate plot
591
+ p <- spatialDimPlot(
592
+ object = obj,
593
+ reduction = "{reduction}",
594
+ group.by = "{group_by}",
595
+ label = {str(label).upper()},
596
+ pt.size.factor = {pt_size}, # Note: R parameter is pt.size.factor
597
+ verbose = {str(verbose).upper()}
598
+ )
599
+
600
+ # Save the plot
601
+ ggsave(
602
+ filename = output_plot_path,
603
+ plot = p,
604
+ width = {width},
605
+ height = {height},
606
+ units = "{units}",
607
+ dpi = {dpi}
608
+ )
609
+
610
+ message(paste("Plot saved to:", output_plot_path))
611
+ """
612
+
613
+ temp_r_script_path: Optional[Path] = None
614
+ try:
615
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script:
616
+ temp_r_script.write(r_script_content)
617
+ temp_r_script_path = Path(temp_r_script.name)
618
+
619
+ command = ["Rscript", str(temp_r_script_path)]
620
+
621
+ # 3. Subprocess execution
622
+ process = subprocess.run(
623
+ command,
624
+ check=True,
625
+ capture_output=True,
626
+ text=True,
627
+ env=os.environ
628
+ )
629
+
630
+ stdout = process.stdout
631
+ stderr = process.stderr
632
+
633
+ # 4. Error handling: Check for R-specific errors in stderr
634
+ if "Error" in stderr or "stop(" in stderr:
635
+ raise RuntimeError(f"R script execution failed. Stderr: {stderr}")
636
+
637
+ if not output_plot_path.exists():
638
+ raise RuntimeError(f"Output plot file was not created by R script: {output_plot_path}")
639
+
640
+ return {
641
+ "command_executed": " ".join(command),
642
+ "stdout": stdout,
643
+ "stderr": stderr,
644
+ "output_files": [str(output_plot_path)],
645
+ }
646
+
647
+ except FileNotFoundError:
648
+ raise RuntimeError("Rscript command not found. Is R installed and in your PATH?")
649
+ except subprocess.CalledProcessError as e:
650
+ raise RuntimeError(
651
+ f"Rscript execution failed with exit code {e.returncode}.\n"
652
+ f"Command: {' '.join(e.cmd)}\n"
653
+ f"Stdout: {e.stdout}\n"
654
+ f"Stderr: {e.stderr}"
655
+ )
656
+ finally:
657
+ # Clean up temporary R script
658
+ if temp_r_script_path and temp_r_script_path.exists():
659
+ os.remove(temp_r_script_path)
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/bioconductor-banksy_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/bioconductor-banksy_server.py')
11
+ SERVER_NAME = 'biosci_bioconductor_banksy'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/app/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ mcp-bioconductor-banksy:
5
+ build: .
6
+ image: mcp-bioconductor-banksy:latest
7
+ container_name: mcp-bioconductor-banksy
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ - MCP_SERVER_NAME=bioconductor-banksy
12
+ volumes:
13
+ - ./workspace:/app/workspace
14
+ - ./output:/app/output
15
+ restart: unless-stopped
16
+ healthcheck:
17
+ test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 5s
22
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/environment.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ name: mcp-tool
3
+ channels:
4
+ - bioconda
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - bioconductor-banksy
9
+ - python=3.10
10
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-banksy/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastmcp
2
+ mcp
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Install Miniconda
8
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh
9
+
10
+ # Add conda to PATH
11
+ ENV PATH="/opt/conda/bin:$PATH"
12
+
13
+ # Install bioconductor-benchdamic via conda (e.g., from bioconda)
14
+ RUN conda install -c bioconda bioconductor-benchdamic -y && conda clean -a
15
+
16
+ # Install Python dependencies
17
+ RUN pip install uv
18
+ RUN uv pip install --system fastmcp
19
+
20
+ # Create app directory
21
+ WORKDIR /app
22
+
23
+ # Copy your MCP server
24
+ COPY app/bioconductor-benchdamic_server.py /app/
25
+
26
+ # Create workspace and output directories
27
+ RUN mkdir -p /app/workspace /app/output
28
+
29
+ # Make sure the server script is executable
30
+ RUN chmod +x /app/bioconductor-benchdamic_server.py
31
+
32
+ # Expose port for MCP over HTTP (optional)
33
+ EXPOSE 8000
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
37
+
38
+ # Default command runs the MCP server via stdio
39
+ CMD ["python", "/app/bioconductor-benchdamic_server.py"]
40
+
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_server.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import tempfile
3
+ from pathlib import Path
4
+ import os
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ # Set up logging
9
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # MCP decorator is assumed to be available in the execution environment.
13
+ # This is a placeholder for the actual decorator.
14
+ class mcp:
15
+ @staticmethod
16
+ def tool(func):
17
+ def wrapper(*args, **kwargs):
18
+ return func(*args, **kwargs)
19
+ return wrapper
20
+
21
+ @mcp.tool
22
+ def run_benchdamic(
23
+ config_file: Path,
24
+ output_dir: Path,
25
+ ):
26
+ """
27
+ Runs the full benchdamic pipeline.
28
+
29
+ This function serves as the main entry point for the benchdamic workflow,
30
+ executing data generation, method application, evaluation, and plotting
31
+ based on a single YAML configuration file. The process is run within the
32
+ specified output directory.
33
+
34
+ Args:
35
+ config_file: Path to the benchdamic YAML configuration file.
36
+ output_dir: Path to the directory where all outputs will be stored.
37
+ It will be created if it doesn't exist.
38
+ """
39
+ # 1. Input validation
40
+ if not config_file.is_file():
41
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
42
+
43
+ # 2. File path handling
44
+ output_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ # The R function benchdamic() relies on the 'output_dir' key within the config YAML.
47
+ # By running Rscript from the specified output_dir, we ensure that
48
+ # relative paths in the config file are resolved correctly.
49
+ r_script_content = f"""
50
+ if (!requireNamespace("benchdamic", quietly = TRUE)) {{
51
+ stop("The 'benchdamic' R package is not installed. Please install it from Bioconductor.")
52
+ }}
53
+ library(benchdamic)
54
+
55
+ config_path <- "{config_file.resolve()}"
56
+
57
+ cat("Starting benchdamic pipeline...\\n")
58
+ benchdamic::benchdamic(config_file = config_path)
59
+ cat("benchdamic pipeline finished successfully.\\n")
60
+ """
61
+
62
+ # 3. Subprocess execution
63
+ command_to_execute = []
64
+ stdout_str = ""
65
+ stderr_str = ""
66
+ r_script_path = None
67
+
68
+ try:
69
+ with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=output_dir) as r_script_file:
70
+ r_script_file.write(r_script_content)
71
+ r_script_path = Path(r_script_file.name)
72
+
73
+ # Use relative path for the command since we set cwd
74
+ command_to_execute = ["Rscript", str(r_script_path.name)]
75
+
76
+ files_before = set(os.listdir(output_dir))
77
+
78
+ process = subprocess.run(
79
+ command_to_execute,
80
+ capture_output=True,
81
+ text=True,
82
+ check=True,
83
+ cwd=output_dir,
84
+ )
85
+
86
+ stdout_str = process.stdout
87
+ stderr_str = process.stderr
88
+
89
+ files_after = set(os.listdir(output_dir))
90
+ # Find new files and directories
91
+ new_items = files_after - files_before
92
+ output_files = [str(output_dir / item) for item in new_items]
93
+
94
+ except FileNotFoundError:
95
+ err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH."
96
+ logger.error(err_msg)
97
+ raise RuntimeError(err_msg) from None
98
+ except subprocess.CalledProcessError as e:
99
+ logger.error(f"R script execution failed with exit code {e.returncode}")
100
+ logger.error(f"Command: {' '.join(command_to_execute)}")
101
+ logger.error(f"Stdout: {e.stdout}")
102
+ logger.error(f"Stderr: {e.stderr}")
103
+ raise
104
+ finally:
105
+ if r_script_path and r_script_path.exists():
106
+ r_script_path.unlink()
107
+
108
+ # 4. Structured result return
109
+ return {
110
+ "command_executed": " ".join(command_to_execute),
111
+ "stdout": stdout_str,
112
+ "stderr": stderr_str,
113
+ "output_files": output_files
114
+ }
115
+
116
+ @mcp.tool
117
+ def generate_data(
118
+ config_file: Path,
119
+ output_dir: Path,
120
+ ):
121
+ """
122
+ Generates simulated data using the benchdamic configuration.
123
+
124
+ This function corresponds to the `generate_data` step in the benchdamic
125
+ workflow. It reads simulation parameters from the config file and writes
126
+ the generated datasets to the specified output directory.
127
+
128
+ Args:
129
+ config_file: Path to the benchdamic YAML configuration file.
130
+ output_dir: Path to the directory where generated data will be stored.
131
+ It will be created if it doesn't exist.
132
+ """
133
+ # 1. Input validation
134
+ if not config_file.is_file():
135
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
136
+
137
+ # 2. File path handling
138
+ output_dir.mkdir(parents=True, exist_ok=True)
139
+
140
+ r_script_content = f"""
141
+ if (!requireNamespace("benchdamic", quietly = TRUE)) {{
142
+ stop("The 'benchdamic' R package is not installed.")
143
+ }}
144
+ library(benchdamic)
145
+
146
+ config_path <- "{config_file.resolve()}"
147
+ output_path <- "{output_dir.resolve()}"
148
+
149
+ cat("Starting data generation...\\n")
150
+ benchdamic::generate_data(config_file = config_path, output_dir = output_path)
151
+ cat("Data generation finished successfully.\\n")
152
+ """
153
+
154
+ # 3. Subprocess execution
155
+ command_to_execute = []
156
+ stdout_str = ""
157
+ stderr_str = ""
158
+ r_script_path = None
159
+
160
+ try:
161
+ with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file:
162
+ r_script_file.write(r_script_content)
163
+ r_script_path = Path(r_script_file.name)
164
+
165
+ command_to_execute = ["Rscript", str(r_script_path)]
166
+
167
+ files_before = set(p.resolve() for p in output_dir.glob('**/*'))
168
+
169
+ process = subprocess.run(
170
+ command_to_execute,
171
+ capture_output=True,
172
+ text=True,
173
+ check=True,
174
+ )
175
+
176
+ stdout_str = process.stdout
177
+ stderr_str = process.stderr
178
+
179
+ files_after = set(p.resolve() for p in output_dir.glob('**/*'))
180
+ new_files = [str(p) for p in files_after - files_before]
181
+
182
+ except FileNotFoundError:
183
+ err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH."
184
+ logger.error(err_msg)
185
+ raise RuntimeError(err_msg) from None
186
+ except subprocess.CalledProcessError as e:
187
+ logger.error(f"R script execution failed with exit code {e.returncode}")
188
+ logger.error(f"Command: {' '.join(command_to_execute)}")
189
+ logger.error(f"Stdout: {e.stdout}")
190
+ logger.error(f"Stderr: {e.stderr}")
191
+ raise
192
+ finally:
193
+ if r_script_path and r_script_path.exists():
194
+ r_script_path.unlink()
195
+
196
+ # 4. Structured result return
197
+ return {
198
+ "command_executed": " ".join(command_to_execute),
199
+ "stdout": stdout_str,
200
+ "stderr": stderr_str,
201
+ "output_files": new_files
202
+ }
203
+
204
+ @mcp.tool
205
+ def run_methods(
206
+ config_file: Path,
207
+ output_dir: Path,
208
+ ):
209
+ """
210
+ Runs differential abundance methods on generated data.
211
+
212
+ This function corresponds to the `run_methods` step in the benchdamic
213
+ workflow. It applies the specified methods to the datasets found in the
214
+ output directory and saves their results.
215
+
216
+ Args:
217
+ config_file: Path to the benchdamic YAML configuration file.
218
+ output_dir: Path to the directory containing the generated data and
219
+ where method results will be stored. It must exist.
220
+ """
221
+ # 1. Input validation
222
+ if not config_file.is_file():
223
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
224
+ if not output_dir.is_dir():
225
+ raise FileNotFoundError(f"Output directory not found: {output_dir}. Please run generate_data first.")
226
+
227
+ # 2. R script content
228
+ r_script_content = f"""
229
+ if (!requireNamespace("benchdamic", quietly = TRUE)) {{
230
+ stop("The 'benchdamic' R package is not installed.")
231
+ }}
232
+ library(benchdamic)
233
+
234
+ config_path <- "{config_file.resolve()}"
235
+ output_path <- "{output_dir.resolve()}"
236
+
237
+ cat("Running differential abundance methods...\\n")
238
+ benchdamic::run_methods(config_file = config_path, output_dir = output_path)
239
+ cat("Method execution finished successfully.\\n")
240
+ """
241
+
242
+ # 3. Subprocess execution
243
+ command_to_execute = []
244
+ stdout_str = ""
245
+ stderr_str = ""
246
+ r_script_path = None
247
+
248
+ try:
249
+ with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file:
250
+ r_script_file.write(r_script_content)
251
+ r_script_path = Path(r_script_file.name)
252
+
253
+ command_to_execute = ["Rscript", str(r_script_path)]
254
+
255
+ files_before = set(p.resolve() for p in output_dir.glob('**/*'))
256
+
257
+ process = subprocess.run(
258
+ command_to_execute,
259
+ capture_output=True,
260
+ text=True,
261
+ check=True,
262
+ )
263
+
264
+ stdout_str = process.stdout
265
+ stderr_str = process.stderr
266
+
267
+ files_after = set(p.resolve() for p in output_dir.glob('**/*'))
268
+ new_files = [str(p) for p in files_after - files_before]
269
+
270
+ except FileNotFoundError:
271
+ err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH."
272
+ logger.error(err_msg)
273
+ raise RuntimeError(err_msg) from None
274
+ except subprocess.CalledProcessError as e:
275
+ logger.error(f"R script execution failed with exit code {e.returncode}")
276
+ logger.error(f"Command: {' '.join(command_to_execute)}")
277
+ logger.error(f"Stdout: {e.stdout}")
278
+ logger.error(f"Stderr: {e.stderr}")
279
+ raise
280
+ finally:
281
+ if r_script_path and r_script_path.exists():
282
+ r_script_path.unlink()
283
+
284
+ # 4. Structured result return
285
+ return {
286
+ "command_executed": " ".join(command_to_execute),
287
+ "stdout": stdout_str,
288
+ "stderr": stderr_str,
289
+ "output_files": new_files
290
+ }
291
+
292
+ @mcp.tool
293
+ def evaluate_methods(
294
+ config_file: Path,
295
+ output_dir: Path,
296
+ ):
297
+ """
298
+ Evaluates the performance of differential abundance methods.
299
+
300
+ This function corresponds to the `evaluate_methods` step in the benchdamic
301
+ workflow. It computes performance metrics based on the method results
302
+ and ground truth, saving the evaluation to the output directory.
303
+
304
+ Args:
305
+ config_file: Path to the benchdamic YAML configuration file.
306
+ output_dir: Path to the directory containing method results. It must exist.
307
+ """
308
+ # 1. Input validation
309
+ if not config_file.is_file():
310
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
311
+ if not output_dir.is_dir():
312
+ raise FileNotFoundError(f"Output directory not found: {output_dir}.")
313
+
314
+ # 2. R script content
315
+ r_script_content = f"""
316
+ if (!requireNamespace("benchdamic", quietly = TRUE)) {{
317
+ stop("The 'benchdamic' R package is not installed.")
318
+ }}
319
+ library(benchdamic)
320
+
321
+ config_path <- "{config_file.resolve()}"
322
+ output_path <- "{output_dir.resolve()}"
323
+
324
+ cat("Evaluating method performance...\\n")
325
+ benchdamic::evaluate_methods(config_file = config_path, output_dir = output_path)
326
+ cat("Evaluation finished successfully.\\n")
327
+ """
328
+
329
+ # 3. Subprocess execution
330
+ command_to_execute = []
331
+ stdout_str = ""
332
+ stderr_str = ""
333
+ r_script_path = None
334
+
335
+ try:
336
+ with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file:
337
+ r_script_file.write(r_script_content)
338
+ r_script_path = Path(r_script_file.name)
339
+
340
+ command_to_execute = ["Rscript", str(r_script_path)]
341
+
342
+ files_before = set(p.resolve() for p in output_dir.glob('**/*'))
343
+
344
+ process = subprocess.run(
345
+ command_to_execute,
346
+ capture_output=True,
347
+ text=True,
348
+ check=True,
349
+ )
350
+
351
+ stdout_str = process.stdout
352
+ stderr_str = process.stderr
353
+
354
+ files_after = set(p.resolve() for p in output_dir.glob('**/*'))
355
+ new_files = [str(p) for p in files_after - files_before]
356
+
357
+ except FileNotFoundError:
358
+ err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH."
359
+ logger.error(err_msg)
360
+ raise RuntimeError(err_msg) from None
361
+ except subprocess.CalledProcessError as e:
362
+ logger.error(f"R script execution failed with exit code {e.returncode}")
363
+ logger.error(f"Command: {' '.join(command_to_execute)}")
364
+ logger.error(f"Stdout: {e.stdout}")
365
+ logger.error(f"Stderr: {e.stderr}")
366
+ raise
367
+ finally:
368
+ if r_script_path and r_script_path.exists():
369
+ r_script_path.unlink()
370
+
371
+ # 4. Structured result return
372
+ return {
373
+ "command_executed": " ".join(command_to_execute),
374
+ "stdout": stdout_str,
375
+ "stderr": stderr_str,
376
+ "output_files": new_files
377
+ }
378
+
379
+ @mcp.tool
380
+ def plot_results(
381
+ config_file: Path,
382
+ output_dir: Path,
383
+ ):
384
+ """
385
+ Generates plots from the evaluation results.
386
+
387
+ This function corresponds to the `plot_results` step in the benchdamic
388
+ workflow. It creates various visualizations of the performance metrics
389
+ and saves them as files in the output directory.
390
+
391
+ Args:
392
+ config_file: Path to the benchdamic YAML configuration file.
393
+ output_dir: Path to the directory containing evaluation results. It must exist.
394
+ """
395
+ # 1. Input validation
396
+ if not config_file.is_file():
397
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
398
+ if not output_dir.is_dir():
399
+ raise FileNotFoundError(f"Output directory not found: {output_dir}.")
400
+
401
+ # 2. R script content
402
+ r_script_content = f"""
403
+ if (!requireNamespace("benchdamic", quietly = TRUE)) {{
404
+ stop("The 'benchdamic' R package is not installed.")
405
+ }}
406
+ library(benchdamic)
407
+
408
+ config_path <- "{config_file.resolve()}"
409
+ output_path <- "{output_dir.resolve()}"
410
+
411
+ cat("Generating plots...\\n")
412
+ benchdamic::plot_results(config_file = config_path, output_dir = output_path)
413
+ cat("Plot generation finished successfully.\\n")
414
+ """
415
+
416
+ # 3. Subprocess execution
417
+ command_to_execute = []
418
+ stdout_str = ""
419
+ stderr_str = ""
420
+ r_script_path = None
421
+
422
+ try:
423
+ with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file:
424
+ r_script_file.write(r_script_content)
425
+ r_script_path = Path(r_script_file.name)
426
+
427
+ command_to_execute = ["Rscript", str(r_script_path)]
428
+
429
+ files_before = set(p.resolve() for p in output_dir.glob('**/*'))
430
+
431
+ process = subprocess.run(
432
+ command_to_execute,
433
+ capture_output=True,
434
+ text=True,
435
+ check=True,
436
+ )
437
+
438
+ stdout_str = process.stdout
439
+ stderr_str = process.stderr
440
+
441
+ files_after = set(p.resolve() for p in output_dir.glob('**/*'))
442
+ new_files = [str(p) for p in files_after - files_before]
443
+
444
+ except FileNotFoundError:
445
+ err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH."
446
+ logger.error(err_msg)
447
+ raise RuntimeError(err_msg) from None
448
+ except subprocess.CalledProcessError as e:
449
+ logger.error(f"R script execution failed with exit code {e.returncode}")
450
+ logger.error(f"Command: {' '.join(command_to_execute)}")
451
+ logger.error(f"Stdout: {e.stdout}")
452
+ logger.error(f"Stderr: {e.stderr}")
453
+ raise
454
+ finally:
455
+ if r_script_path and r_script_path.exists():
456
+ r_script_path.unlink()
457
+
458
+ # 4. Structured result return
459
+ return {
460
+ "command_executed": " ".join(command_to_execute),
461
+ "stdout": stdout_str,
462
+ "stderr": stderr_str,
463
+ "output_files": new_files
464
+ }
BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_shim_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+
10
+ SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_server.py')
11
+ SERVER_NAME = 'biosci_bioconductor_benchdamic'
12
+
13
+
14
+ class _ShimMCP:
15
+ @staticmethod
16
+ def tool():
17
+ def _decorator(fn):
18
+ return fn
19
+ return _decorator
20
+
21
+
22
+ def _load_functions():
23
+ code = SOURCE_SERVER.read_text(encoding="utf-8")
24
+ tree = ast.parse(code, filename=str(SOURCE_SERVER))
25
+ function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
26
+ namespace = {
27
+ "__name__": "__mcp_source__",
28
+ "mcp": _ShimMCP(),
29
+ }
30
+ exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace)
31
+ loaded = []
32
+ for name in function_names:
33
+ fn = namespace.get(name)
34
+ if callable(fn):
35
+ loaded.append(fn)
36
+ return loaded
37
+
38
+
39
+ mcp = FastMCP(SERVER_NAME)
40
+ for _fn in _load_functions():
41
+ mcp.tool()(_fn)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ mcp.run(transport="stdio")