File size: 14,001 Bytes
2862b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3b49d6
2862b00
8d71444
2862b00
 
 
c3b49d6
2862b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3b49d6
2862b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3b49d6
2862b00
 
 
 
c3b49d6
2862b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3b49d6
2862b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
"""
Decoupler enrichment analysis tutorial demonstrating observation-level enrichment scoring.

This MCP Server provides 4 tools:
1. decoupler_load_toy_data: Load toy dataset and visualize gene expression patterns
2. decoupler_run_individual_methods: Run individual enrichment methods (GSEA and ULM)
3. decoupler_run_multiple_methods: Run all enrichment methods and compute consensus scores
4. decoupler_access_prior_knowledge: Query OmniPath resources for prior knowledge networks

All tools extracted from `scverse/decoupler-tutorials/blob/main/example.ipynb`.
"""

import os
from datetime import datetime
from pathlib import Path

# Standard imports
from typing import Annotated

import anndata as ad
import decoupler as dc

# Domain-specific imports
import matplotlib.pyplot as plt
import pandas as pd
import scanpy as sc
from fastmcp import FastMCP

# Project structure
PROJECT_ROOT = Path(__file__).parent.parent.parent.resolve()
DEFAULT_INPUT_DIR = PROJECT_ROOT / "tmp" / "inputs"
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "tmp" / "outputs"

INPUT_DIR = Path(os.environ.get("EXAMPLE_INPUT_DIR", DEFAULT_INPUT_DIR))
OUTPUT_DIR = Path(os.environ.get("EXAMPLE_OUTPUT_DIR", DEFAULT_OUTPUT_DIR))

# Ensure directories exist
INPUT_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# Timestamp for unique outputs
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

# Configure plotting parameters
plt.rcParams["figure.dpi"] = 300
plt.rcParams["savefig.dpi"] = 300
sc.set_figure_params(figsize=(3.5, 3.5), frameon=False, dpi=300)

# MCP server instance
example_mcp = FastMCP(name="example")


@example_mcp.tool
def decoupler_load_toy_data(
    out_prefix: Annotated[str | None, "Output file prefix"] = None,
) -> dict:
    """
    Load decoupler toy dataset and visualize gene expression patterns across cell groups.
    Input is no primary data (uses built-in toy dataset) and output is AnnData object with heatmap and violin plot visualizations.
    """

    # Set matplotlib backend for non-interactive plotting
    plt.switch_backend("Agg")

    # Set output prefix
    if out_prefix is None:
        out_prefix = f"toy_data_{timestamp}"

    # Load toy dataset
    adata, net = dc.ds.toy()

    # Save the AnnData object and network
    adata_path = OUTPUT_DIR / f"{out_prefix}_adata.h5ad"
    net_path = OUTPUT_DIR / f"{out_prefix}_network.csv"

    adata.write(adata_path)
    net.to_csv(net_path, index=False)

    # Generate heatmap visualization
    heatmap_path = OUTPUT_DIR / f"{out_prefix}_gene_expression_heatmap.png"
    plt.figure(figsize=(8, 6))
    sc.pl.heatmap(adata=adata, groupby="group", var_names=adata.var_names, show=False)
    plt.savefig(heatmap_path, dpi=300, bbox_inches="tight")
    plt.close("all")

    # Generate violin plot
    violin_path = OUTPUT_DIR / f"{out_prefix}_gene_expression_violin.png"
    plt.figure(figsize=(8, 6))
    sc.pl.violin(adata, groupby="group", keys=["G01", "G05", "G13"], show=False)
    plt.savefig(violin_path, dpi=300, bbox_inches="tight")
    plt.close("all")

    # Generate network visualization
    network_path = OUTPUT_DIR / f"{out_prefix}_network_plot.png"
    plt.figure(figsize=(5, 5))
    dc.pl.network(
        net,
        size_node=15,
        s_cmap="white",
        t_cmap="white",
        c_pos_w="darkgreen",
        c_neg_w="darkred",
        return_fig=True,
    )
    plt.savefig(network_path, dpi=300, bbox_inches="tight")
    plt.close("all")

    plt.close("all")  # Close all figures to free memory

    return {
        "message": f"Toy dataset loaded and visualized with {adata.n_obs} cells and {adata.n_vars} genes",
        "reference": "https://github.com/scverse/decoupler-tutorials/blob/main/example.ipynb",
        "artifacts": [
            {
                "description": "AnnData object with expression data",
                "path": str(adata_path.resolve()),
            },
            {"description": "Prior knowledge network", "path": str(net_path.resolve())},
            {
                "description": "Gene expression heatmap",
                "path": str(heatmap_path.resolve()),
            },
            {
                "description": "Gene expression violin plot",
                "path": str(violin_path.resolve()),
            },
            {
                "description": "Network visualization",
                "path": str(network_path.resolve()),
            },
        ],
    }


@example_mcp.tool
def decoupler_run_individual_methods(
    adata_path: Annotated[str | None, "Path to AnnData file with expression data"] = None,
    network_path: Annotated[
        str | None,
        "Path to network file in CSV format with columns: source, target, weight",
    ] = None,
    tmin: Annotated[int, "Minimum number of targets per source"] = 0,
    out_prefix: Annotated[str | None, "Output file prefix"] = None,
) -> dict:
    """
    Run individual enrichment methods (GSEA and ULM) on gene expression data using prior knowledge networks.
    Input is AnnData expression file and network file and output is enrichment scores with heatmap visualizations.
    """

    # Set matplotlib backend for non-interactive plotting
    plt.switch_backend("Agg")

    # Input validation
    if adata_path is None:
        raise ValueError("Path to AnnData file must be provided")
    if network_path is None:
        raise ValueError("Path to network file must be provided")

    # File existence validation
    adata_file = Path(adata_path)
    if not adata_file.exists():
        raise FileNotFoundError(f"AnnData file not found: {adata_path}")

    network_file = Path(network_path)
    if not network_file.exists():
        raise FileNotFoundError(f"Network file not found: {network_path}")

    # Set output prefix
    if out_prefix is None:
        out_prefix = f"individual_methods_{timestamp}"

    # Load data
    adata = ad.read_h5ad(adata_path)
    net = pd.read_csv(network_path)

    # Run GSEA method
    dc.mt.gsea(data=adata, net=net, tmin=tmin)

    # Visualize GSEA scores
    scores_gsea = dc.pp.get_obsm(adata, key="score_gsea")
    gsea_heatmap_path = OUTPUT_DIR / f"{out_prefix}_gsea_scores_heatmap.png"
    plt.figure(figsize=(8, 6))
    sc.pl.heatmap(
        adata=scores_gsea,
        groupby="group",
        var_names=scores_gsea.var_names,
        cmap="RdBu_r",
        vcenter=0,
        show=False,
    )
    plt.savefig(gsea_heatmap_path, dpi=300, bbox_inches="tight")
    plt.close("all")

    # Run ULM method
    dc.mt.ulm(data=adata, net=net, tmin=tmin)

    # Visualize ULM scores
    scores_ulm = dc.pp.get_obsm(adata, key="score_ulm")
    ulm_heatmap_path = OUTPUT_DIR / f"{out_prefix}_ulm_scores_heatmap.png"
    plt.figure(figsize=(8, 6))
    sc.pl.heatmap(
        adata=scores_ulm,
        groupby="group",
        var_names=scores_ulm.var_names,
        cmap="RdBu_r",
        vcenter=0,
        show=False,
    )
    plt.savefig(ulm_heatmap_path, dpi=300, bbox_inches="tight")
    plt.close("all")

    plt.close("all")  # Close all figures to free memory

    # Save results
    adata_results_path = OUTPUT_DIR / f"{out_prefix}_adata_with_scores.h5ad"
    adata.write(adata_results_path)

    # Save score matrices as CSV
    gsea_scores_path = OUTPUT_DIR / f"{out_prefix}_gsea_scores.csv"
    scores_gsea.to_df().to_csv(gsea_scores_path)

    ulm_scores_path = OUTPUT_DIR / f"{out_prefix}_ulm_scores.csv"
    scores_ulm.to_df().to_csv(ulm_scores_path)

    return {
        "message": f"Individual enrichment methods completed: GSEA and ULM on {adata.n_obs} cells",
        "reference": "https://github.com/scverse/decoupler-tutorials/blob/main/example.ipynb",
        "artifacts": [
            {
                "description": "AnnData with enrichment scores",
                "path": str(adata_results_path.resolve()),
            },
            {
                "description": "GSEA enrichment scores",
                "path": str(gsea_scores_path.resolve()),
            },
            {
                "description": "ULM enrichment scores",
                "path": str(ulm_scores_path.resolve()),
            },
            {
                "description": "GSEA scores heatmap",
                "path": str(gsea_heatmap_path.resolve()),
            },
            {
                "description": "ULM scores heatmap",
                "path": str(ulm_heatmap_path.resolve()),
            },
        ],
    }


@example_mcp.tool
def decoupler_run_multiple_methods(
    adata_path: Annotated[str | None, "Path to AnnData file with expression data"] = None,
    network_path: Annotated[
        str | None,
        "Path to network file in CSV format with columns: source, target, weight",
    ] = None,
    methods: Annotated[str, "Methods to run - use 'all' for all available methods"] = "all",
    tmin: Annotated[int, "Minimum number of targets per source"] = 0,
    out_prefix: Annotated[str | None, "Output file prefix"] = None,
) -> dict:
    """
    Run multiple enrichment methods and compute consensus scores across methods for gene expression analysis.
    Input is AnnData expression file and network file and output is consensus enrichment scores with heatmap visualization.
    """

    # Set matplotlib backend for non-interactive plotting
    plt.switch_backend("Agg")

    # Input validation
    if adata_path is None:
        raise ValueError("Path to AnnData file must be provided")
    if network_path is None:
        raise ValueError("Path to network file must be provided")

    # File existence validation
    adata_file = Path(adata_path)
    if not adata_file.exists():
        raise FileNotFoundError(f"AnnData file not found: {adata_path}")

    network_file = Path(network_path)
    if not network_file.exists():
        raise FileNotFoundError(f"Network file not found: {network_path}")

    # Set output prefix
    if out_prefix is None:
        out_prefix = f"multiple_methods_{timestamp}"

    # Load data
    adata = ad.read_h5ad(adata_path)
    net = pd.read_csv(network_path)

    # Run multiple methods
    # Handle methods parameter - convert to list if needed to avoid system crashes
    if methods == "all":
        # Use a stable subset to prevent system crashes with all methods
        methods_to_use = ["gsea", "ulm", "mlm", "viper"]
    else:
        methods_to_use = methods

    dc.mt.decouple(
        data=adata,
        net=net,
        methods=methods_to_use,
        tmin=tmin,
    )

    # Compute consensus scores
    dc.mt.consensus(result=adata)

    # Visualize consensus scores
    scores_consensus = dc.pp.get_obsm(adata, key="score_consensus")
    consensus_heatmap_path = OUTPUT_DIR / f"{out_prefix}_consensus_scores_heatmap.png"
    plt.figure(figsize=(8, 6))
    sc.pl.heatmap(
        adata=scores_consensus,
        groupby="group",
        var_names=scores_consensus.var_names,
        cmap="RdBu_r",
        vcenter=0,
        show=False,
    )
    plt.savefig(consensus_heatmap_path, dpi=300, bbox_inches="tight")
    plt.close("all")

    plt.close("all")  # Close all figures to free memory

    # Save results
    adata_results_path = OUTPUT_DIR / f"{out_prefix}_adata_all_methods.h5ad"
    adata.write(adata_results_path)

    # Save consensus scores as CSV
    consensus_scores_path = OUTPUT_DIR / f"{out_prefix}_consensus_scores.csv"
    scores_consensus.to_df().to_csv(consensus_scores_path)

    return {
        "message": f"Multiple enrichment methods completed with consensus scoring on {adata.n_obs} cells",
        "reference": "https://github.com/scverse/decoupler-tutorials/blob/main/example.ipynb",
        "artifacts": [
            {
                "description": "AnnData with all method scores",
                "path": str(adata_results_path.resolve()),
            },
            {
                "description": "Consensus enrichment scores",
                "path": str(consensus_scores_path.resolve()),
            },
            {
                "description": "Consensus scores heatmap",
                "path": str(consensus_heatmap_path.resolve()),
            },
        ],
    }


@example_mcp.tool
def decoupler_access_prior_knowledge(
    resource_name: Annotated[
        str | None, "Name of OmniPath resource to query (e.g., 'SIGNOR')"
    ] = None,
    out_prefix: Annotated[str | None, "Output file prefix"] = None,
) -> dict:
    """
    Query OmniPath resources to access prior knowledge networks for enrichment analysis.
    Input is resource name and output is available resources list and specific resource data if requested.
    """

    # Set output prefix
    if out_prefix is None:
        out_prefix = f"prior_knowledge_{timestamp}"

    # Get available resources
    resources = dc.op.show_resources()

    # Save resources list
    resources_path = OUTPUT_DIR / f"{out_prefix}_available_resources.csv"
    resources.to_csv(resources_path, index=False)

    artifacts = [
        {
            "description": "Available OmniPath resources",
            "path": str(resources_path.resolve()),
        }
    ]

    message = f"Found {len(resources)} available OmniPath resources"

    # If specific resource requested, download it
    if resource_name is not None:
        try:
            resource_data = dc.op.resource(resource_name)
            resource_data_path = OUTPUT_DIR / f"{out_prefix}_{resource_name}_network.csv"
            resource_data.to_csv(resource_data_path, index=False)

            artifacts.append(
                {
                    "description": f"{resource_name} network data",
                    "path": str(resource_data_path.resolve()),
                }
            )

            message += f" and downloaded {resource_name} with {len(resource_data)} interactions"

        except Exception as e:
            message += f" but failed to download {resource_name}: {str(e)}"

    return {
        "message": message,
        "reference": "https://github.com/scverse/decoupler-tutorials/blob/main/example.ipynb",
        "artifacts": artifacts,
    }