File size: 8,788 Bytes
d9bc96c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import subprocess
from pathlib import Path
from typing import Optional, List

from mcp.server.fastmcp import FastMCP

SERVER_NAME = 'local_bin2cell'
mcp = FastMCP(SERVER_NAME)

@mcp.tool()
def bin2cell_prepare_bins(
    input_dir: str,
    output_h5ad: str,
    bin_size: int = 2,
    destripe: bool = True,
):
    """
    Reads Visium HD data from a SpaceRanger output directory and optionally performs destriping 
    to correct for technical effects in 2um bin data.

    Args:
        input_dir: Path to the SpaceRanger output directory (containing 'outs' folder).
        output_h5ad: Path where the processed bin-level AnnData object will be saved.
        bin_size: Resolution of bins to load (default is 2um).
        destripe: Whether to apply the destriping correction for variable bin dimensions.
    """
    # Input validation
    input_path = Path(input_dir)
    if not input_path.exists():
        return {"error": f"Input directory {input_dir} does not exist."}
    
    output_path = Path(output_h5ad)
    if not output_path.parent.exists():
        output_path.parent.mkdir(parents=True, exist_ok=True)

    if bin_size <= 0:
        return {"error": "bin_size must be a positive integer."}

    # Construct Python command
    # We use a python script string to execute the library functions
    destripe_cmd = "b2c.pp.destripe(adata)" if destripe else "pass"
    python_script = f"""
import bin2cell as b2c
import scanpy as sc
import os

try:
    # Load Visium HD data
    adata = b2c.pp.read_visium_hd_folder('{input_dir}', bin_size={bin_size})
    
    # Perform destriping if requested
    if {destripe}:
        b2c.pp.destripe(adata)
    
    # Save the result
    adata.write('{output_h5ad}')
    print("Successfully prepared bin data.")
except Exception as e:
    print(f"Error: {{str(e)}}")
    exit(1)
"""

    try:
        result = subprocess.run(
            ["python", "-c", python_script],
            capture_output=True,
            text=True,
            check=True
        )
        return {
            "command_executed": f"bin2cell.pp.read_visium_hd_folder and destripe={destripe}",
            "stdout": result.stdout,
            "stderr": result.stderr,
            "output_files": [output_h5ad]
        }
    except subprocess.CalledProcessError as e:
        return {
            "error": "Failed to prepare bin data",
            "stdout": e.stdout,
            "stderr": e.stderr,
            "command_executed": e.cmd
        }

@mcp.tool()
def bin2cell_run_stardist(
    image_path: str,
    output_mask_path: str,
    model_name: str = "2D_versatile_he",
    prob_thresh: float = 0.5,
    nms_thresh: float = 0.3,
):
    """
    Performs cell segmentation on a morphology image using StarDist.

    Args:
        image_path: Path to the high-resolution morphology image (e.g., tissue_hires_image.png).
        output_mask_path: Path where the resulting segmentation mask (.tif) will be saved.
        model_name: StarDist model to use (default: '2D_versatile_he').
        prob_thresh: Probability threshold for StarDist detection.
        nms_thresh: Non-maximum suppression threshold for StarDist.
    """
    # Input validation
    img_path = Path(image_path)
    if not img_path.exists():
        return {"error": f"Image file {image_path} does not exist."}
    
    out_mask = Path(output_mask_path)
    if not out_mask.parent.exists():
        out_mask.parent.mkdir(parents=True, exist_ok=True)

    python_script = f"""
import bin2cell as b2c
import cv2
import numpy as np

try:
    # Run StarDist segmentation via bin2cell wrapper
    # Note: bin2cell.tl.stardist handles the model loading and prediction
    b2c.tl.stardist(
        '{image_path}', 
        '{output_mask_path}', 
        model='{model_name}', 
        prob_thresh={prob_thresh}, 
        nms_thresh={nms_thresh}
    )
    print("Successfully generated segmentation mask.")
except Exception as e:
    print(f"Error: {{str(e)}}")
    exit(1)
"""

    try:
        result = subprocess.run(
            ["python", "-c", python_script],
            capture_output=True,
            text=True,
            check=True
        )
        return {
            "command_executed": f"bin2cell.tl.stardist on {image_path}",
            "stdout": result.stdout,
            "stderr": result.stderr,
            "output_files": [output_mask_path]
        }
    except subprocess.CalledProcessError as e:
        return {
            "error": "StarDist segmentation failed",
            "stdout": e.stdout,
            "stderr": e.stderr,
            "command_executed": e.cmd
        }

@mcp.tool()
def bin2cell_extract_cells(
    bin_h5ad_path: str,
    mask_path: str,
    output_cell_h5ad_path: str,
    qc_metrics: bool = True,
):
    """
    Groups subcellular bins into cells based on a segmentation mask and generates a cell-level AnnData object.

    Args:
        bin_h5ad_path: Path to the bin-level AnnData object (output from bin2cell_prepare_bins).
        mask_path: Path to the segmentation mask file (output from bin2cell_run_stardist).
        output_cell_h5ad_path: Path where the final cell-level AnnData object will be saved.
        qc_metrics: Whether to calculate standard scanpy QC metrics for the new cell object.
    """
    # Input validation
    bin_path = Path(bin_h5ad_path)
    if not bin_path.exists():
        return {"error": f"Bin h5ad file {bin_h5ad_path} does not exist."}
    
    m_path = Path(mask_path)
    if not m_path.exists():
        return {"error": f"Mask file {mask_path} does not exist."}

    out_cell_path = Path(output_cell_h5ad_path)
    if not out_cell_path.parent.exists():
        out_cell_path.parent.mkdir(parents=True, exist_ok=True)

    python_script = f"""
import bin2cell as b2c
import scanpy as sc

try:
    # Load the bin-level data
    adata_bins = sc.read_h5ad('{bin_h5ad_path}')
    
    # Extract cells based on the mask
    adata_cells = b2c.tl.extract_cells(adata_bins, '{mask_path}')
    
    # Calculate QC metrics if requested
    if {qc_metrics}:
        sc.pp.calculate_qc_metrics(adata_cells, inplace=True)
    
    # Save the cell-level object
    adata_cells.write('{output_cell_h5ad_path}')
    print("Successfully extracted cells from bins.")
except Exception as e:
    print(f"Error: {{str(e)}}")
    exit(1)
"""

    try:
        result = subprocess.run(
            ["python", "-c", python_script],
            capture_output=True,
            text=True,
            check=True
        )
        return {
            "command_executed": f"bin2cell.tl.extract_cells using mask {mask_path}",
            "stdout": result.stdout,
            "stderr": result.stderr,
            "output_files": [output_cell_h5ad_path]
        }
    except subprocess.CalledProcessError as e:
        return {
            "error": "Cell extraction failed",
            "stdout": e.stdout,
            "stderr": e.stderr,
            "command_executed": e.cmd
        }

@mcp.tool()
def bin2cell_visualize_segmentation(
    bin_h5ad_path: str,
    output_image_path: str,
    basis: str = "spatial",
):
    """
    Generates a visualization of the bin-to-cell assignments.

    Args:
        bin_h5ad_path: Path to the bin-level AnnData object containing cell assignments.
        output_image_path: Path to save the visualization plot (e.g., .png or .pdf).
        basis: The coordinate system to use for plotting (default: 'spatial').
    """
    bin_path = Path(bin_h5ad_path)
    if not bin_path.exists():
        return {"error": f"Bin h5ad file {bin_h5ad_path} does not exist."}

    python_script = f"""
import scanpy as sc
import matplotlib.pyplot as plt
import bin2cell as b2c

try:
    adata = sc.read_h5ad('{bin_h5ad_path}')
    if 'cell_id' not in adata.obs.columns:
        print("Error: cell_id not found in adata.obs. Run extract_cells first.")
        exit(1)
        
    # Plotting logic
    sc.pl.embedding(adata, basis='{basis}', color='cell_id', show=False)
    plt.savefig('{output_image_path}')
    print("Successfully saved visualization.")
except Exception as e:
    print(f"Error: {{str(e)}}")
    exit(1)
"""

    try:
        result = subprocess.run(
            ["python", "-c", python_script],
            capture_output=True,
            text=True,
            check=True
        )
        return {
            "command_executed": f"Visualization of cell_id on {basis}",
            "stdout": result.stdout,
            "stderr": result.stderr,
            "output_files": [output_image_path]
        }
    except subprocess.CalledProcessError as e:
        return {
            "error": "Visualization failed",
            "stdout": e.stdout,
            "stderr": e.stderr,
            "command_executed": e.cmd
        }

if __name__ == "__main__":
    mcp.run(transport="stdio")