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")