encode-toolkit 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- encode_connector/__init__.py +4 -0
- encode_connector/__main__.py +5 -0
- encode_connector/client/__init__.py +6 -0
- encode_connector/client/auth.py +262 -0
- encode_connector/client/constants.py +450 -0
- encode_connector/client/downloader.py +305 -0
- encode_connector/client/encode_client.py +586 -0
- encode_connector/client/models.py +367 -0
- encode_connector/client/tracker.py +1129 -0
- encode_connector/client/validation.py +226 -0
- encode_connector/server/__init__.py +1 -0
- encode_connector/server/__main__.py +5 -0
- encode_connector/server/main.py +1544 -0
- encode_toolkit-0.3.0.dist-info/METADATA +843 -0
- encode_toolkit-0.3.0.dist-info/RECORD +18 -0
- encode_toolkit-0.3.0.dist-info/WHEEL +4 -0
- encode_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
- encode_toolkit-0.3.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,1544 @@
|
|
|
1
|
+
"""ENCODE Project MCP Server.
|
|
2
|
+
|
|
3
|
+
Exposes ENCODE REST API as Claude-compatible tools for searching experiments,
|
|
4
|
+
listing files, and downloading genomics data.
|
|
5
|
+
|
|
6
|
+
All data stays local. Only connects to encodeproject.org over HTTPS.
|
|
7
|
+
No telemetry, no analytics, no data sent elsewhere.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
from mcp.types import ToolAnnotations
|
|
20
|
+
|
|
21
|
+
from encode_connector.client.auth import CredentialManager
|
|
22
|
+
from encode_connector.client.constants import (
|
|
23
|
+
ASSAY_TITLES,
|
|
24
|
+
BIOSAMPLE_CLASSIFICATIONS,
|
|
25
|
+
ORGAN_SLIMS,
|
|
26
|
+
)
|
|
27
|
+
from encode_connector.client.downloader import FileDownloader
|
|
28
|
+
from encode_connector.client.encode_client import EncodeClient
|
|
29
|
+
from encode_connector.client.models import _human_size
|
|
30
|
+
from encode_connector.client.tracker import (
|
|
31
|
+
ExperimentTracker,
|
|
32
|
+
parse_encode_pipelines,
|
|
33
|
+
parse_encode_publications,
|
|
34
|
+
)
|
|
35
|
+
from encode_connector.client.validation import (
|
|
36
|
+
check_filter_value,
|
|
37
|
+
clamp_limit,
|
|
38
|
+
validate_accession,
|
|
39
|
+
validate_data_export_format,
|
|
40
|
+
validate_encode_path,
|
|
41
|
+
validate_export_format,
|
|
42
|
+
validate_organize_by,
|
|
43
|
+
validate_reference_type,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
# --- Safety annotations for MCP tools ---
|
|
49
|
+
# Read-only tools that query the ENCODE API (network access, no local writes)
|
|
50
|
+
_READONLY_API = ToolAnnotations(
|
|
51
|
+
readOnlyHint=True,
|
|
52
|
+
destructiveHint=False,
|
|
53
|
+
idempotentHint=True,
|
|
54
|
+
openWorldHint=True,
|
|
55
|
+
)
|
|
56
|
+
# Read-only tools that read from local tracker DB only (no network)
|
|
57
|
+
_READONLY_LOCAL = ToolAnnotations(
|
|
58
|
+
readOnlyHint=True,
|
|
59
|
+
destructiveHint=False,
|
|
60
|
+
idempotentHint=True,
|
|
61
|
+
openWorldHint=False,
|
|
62
|
+
)
|
|
63
|
+
# Tools that write to local tracker DB (no network)
|
|
64
|
+
_WRITE_LOCAL = ToolAnnotations(
|
|
65
|
+
readOnlyHint=False,
|
|
66
|
+
destructiveHint=False,
|
|
67
|
+
idempotentHint=True,
|
|
68
|
+
openWorldHint=False,
|
|
69
|
+
)
|
|
70
|
+
# Tools that write to local tracker DB AND query ENCODE API (network)
|
|
71
|
+
_WRITE_LOCAL_API = ToolAnnotations(
|
|
72
|
+
readOnlyHint=False,
|
|
73
|
+
destructiveHint=False,
|
|
74
|
+
idempotentHint=True,
|
|
75
|
+
openWorldHint=True,
|
|
76
|
+
)
|
|
77
|
+
# Tools that download files to disk (network + disk writes)
|
|
78
|
+
_DOWNLOAD = ToolAnnotations(
|
|
79
|
+
readOnlyHint=False,
|
|
80
|
+
destructiveHint=False,
|
|
81
|
+
idempotentHint=True,
|
|
82
|
+
openWorldHint=True,
|
|
83
|
+
)
|
|
84
|
+
# Credential management (can clear credentials = destructive)
|
|
85
|
+
_CREDENTIAL_MGMT = ToolAnnotations(
|
|
86
|
+
readOnlyHint=False,
|
|
87
|
+
destructiveHint=True,
|
|
88
|
+
idempotentHint=True,
|
|
89
|
+
openWorldHint=False,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate_filters(
|
|
94
|
+
assay_title: str | None = None,
|
|
95
|
+
organ: str | None = None,
|
|
96
|
+
biosample_type: str | None = None,
|
|
97
|
+
) -> list[str]:
|
|
98
|
+
"""Validate common filter parameters against known ENCODE values.
|
|
99
|
+
|
|
100
|
+
Returns a list of warning strings (empty if all values are valid).
|
|
101
|
+
"""
|
|
102
|
+
warnings: list[str] = []
|
|
103
|
+
if assay_title:
|
|
104
|
+
warning = check_filter_value(assay_title, ASSAY_TITLES, "assay_title")
|
|
105
|
+
if warning:
|
|
106
|
+
warnings.append(warning)
|
|
107
|
+
if organ:
|
|
108
|
+
warning = check_filter_value(organ, ORGAN_SLIMS, "organ")
|
|
109
|
+
if warning:
|
|
110
|
+
warnings.append(warning)
|
|
111
|
+
if biosample_type:
|
|
112
|
+
warning = check_filter_value(biosample_type, BIOSAMPLE_CLASSIFICATIONS, "biosample_type")
|
|
113
|
+
if warning:
|
|
114
|
+
warnings.append(warning)
|
|
115
|
+
return warnings
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Global client instances (managed via lifespan)
|
|
119
|
+
_client: EncodeClient | None = None
|
|
120
|
+
_downloader: FileDownloader | None = None
|
|
121
|
+
_credential_manager = CredentialManager()
|
|
122
|
+
_tracker: ExperimentTracker | None = None
|
|
123
|
+
_client_lock: asyncio.Lock | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _get_client_lock() -> asyncio.Lock:
|
|
127
|
+
"""Get or create the client lock in the current event loop."""
|
|
128
|
+
global _client_lock
|
|
129
|
+
if _client_lock is None:
|
|
130
|
+
_client_lock = asyncio.Lock()
|
|
131
|
+
return _client_lock
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@asynccontextmanager
|
|
135
|
+
async def lifespan(server: FastMCP):
|
|
136
|
+
"""Manage client lifecycle."""
|
|
137
|
+
global _client, _downloader, _tracker, _client_lock
|
|
138
|
+
# Initialize lock eagerly in lifespan so it's always bound to the
|
|
139
|
+
# correct event loop, rather than lazy-creating in _get_client_lock()
|
|
140
|
+
_client_lock = asyncio.Lock()
|
|
141
|
+
_client = EncodeClient(credential_manager=_credential_manager)
|
|
142
|
+
_downloader = FileDownloader(credential_manager=_credential_manager)
|
|
143
|
+
_tracker = ExperimentTracker()
|
|
144
|
+
try:
|
|
145
|
+
yield
|
|
146
|
+
finally:
|
|
147
|
+
if _client:
|
|
148
|
+
await _client.close()
|
|
149
|
+
if _tracker:
|
|
150
|
+
_tracker.close()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
mcp = FastMCP(
|
|
154
|
+
"ENCODE Project",
|
|
155
|
+
instructions=(
|
|
156
|
+
"Query and download genomics data from the ENCODE Project (encodeproject.org). "
|
|
157
|
+
"Search experiments by assay type, organism, organ, biosample, target, and more. "
|
|
158
|
+
"List and download files (FASTQ, BAM, BED, bigWig, etc.). "
|
|
159
|
+
"All data stays local - no telemetry or external data sharing."
|
|
160
|
+
),
|
|
161
|
+
lifespan=lifespan,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def _get_client() -> EncodeClient:
|
|
166
|
+
async with _get_client_lock():
|
|
167
|
+
if _client is None:
|
|
168
|
+
raise RuntimeError("ENCODE client not initialized")
|
|
169
|
+
return _client
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_downloader() -> FileDownloader:
|
|
173
|
+
if _downloader is None:
|
|
174
|
+
raise RuntimeError("File downloader not initialized")
|
|
175
|
+
return _downloader
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _serialize(obj: Any) -> Any:
|
|
179
|
+
"""Serialize Pydantic models and other objects to JSON-compatible dicts."""
|
|
180
|
+
if hasattr(obj, "model_dump"):
|
|
181
|
+
return obj.model_dump()
|
|
182
|
+
if isinstance(obj, list):
|
|
183
|
+
return [_serialize(item) for item in obj]
|
|
184
|
+
if isinstance(obj, dict):
|
|
185
|
+
return {k: _serialize(v) for k, v in obj.items()}
|
|
186
|
+
return obj
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ======================================================================
|
|
190
|
+
# Tool 1: Search Experiments
|
|
191
|
+
# ======================================================================
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@mcp.tool(annotations=_READONLY_API, title="Search ENCODE Experiments")
|
|
195
|
+
async def encode_search_experiments(
|
|
196
|
+
assay_title: str | None = None,
|
|
197
|
+
organism: str = "Homo sapiens",
|
|
198
|
+
organ: str | None = None,
|
|
199
|
+
biosample_type: str | None = None,
|
|
200
|
+
biosample_term_name: str | None = None,
|
|
201
|
+
target: str | None = None,
|
|
202
|
+
status: str = "released",
|
|
203
|
+
lab: str | None = None,
|
|
204
|
+
award: str | None = None,
|
|
205
|
+
assembly: str | None = None,
|
|
206
|
+
replication_type: str | None = None,
|
|
207
|
+
life_stage: str | None = None,
|
|
208
|
+
sex: str | None = None,
|
|
209
|
+
treatment: str | None = None,
|
|
210
|
+
genetic_modification: str | None = None,
|
|
211
|
+
perturbed: bool | None = None,
|
|
212
|
+
search_term: str | None = None,
|
|
213
|
+
date_released_from: str | None = None,
|
|
214
|
+
date_released_to: str | None = None,
|
|
215
|
+
limit: int = 25,
|
|
216
|
+
offset: int = 0,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Search ENCODE experiments with comprehensive filters.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
- Find all Histone ChIP-seq on human pancreas tissue:
|
|
222
|
+
assay_title="Histone ChIP-seq", organ="pancreas", biosample_type="tissue"
|
|
223
|
+
- Find ATAC-seq on human brain:
|
|
224
|
+
assay_title="ATAC-seq", organ="brain"
|
|
225
|
+
- Find RNA-seq on GM12878 cell line:
|
|
226
|
+
assay_title="total RNA-seq", biosample_term_name="GM12878"
|
|
227
|
+
- Find ChIP-seq targeting H3K27me3:
|
|
228
|
+
assay_title="Histone ChIP-seq", target="H3K27me3"
|
|
229
|
+
- Find all mouse liver experiments:
|
|
230
|
+
organism="Mus musculus", organ="liver"
|
|
231
|
+
- Free text search:
|
|
232
|
+
search_term="CRISPR screen pancreatic"
|
|
233
|
+
|
|
234
|
+
Common assay_title values: "Histone ChIP-seq", "TF ChIP-seq", "ATAC-seq",
|
|
235
|
+
"DNase-seq", "total RNA-seq", "polyA plus RNA-seq", "WGBS", "intact Hi-C",
|
|
236
|
+
"CUT&RUN", "CUT&Tag", "STARR-seq", "MPRA", "eCLIP", "CRISPR screen"
|
|
237
|
+
|
|
238
|
+
Common organ values: "pancreas", "liver", "brain", "heart", "kidney",
|
|
239
|
+
"lung", "intestine", "skin of body", "blood", "spleen", "thymus"
|
|
240
|
+
|
|
241
|
+
biosample_type values: "tissue", "cell line", "primary cell",
|
|
242
|
+
"in vitro differentiated cells", "organoid"
|
|
243
|
+
|
|
244
|
+
WHEN TO USE: Use as the primary entry point when users want to find experiments.
|
|
245
|
+
Start with encode_get_facets if unsure what filters to use.
|
|
246
|
+
RELATED TOOLS: encode_get_facets, encode_get_metadata, encode_search_files
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
assay_title: Assay type (e.g., "Histone ChIP-seq", "ATAC-seq", "total RNA-seq")
|
|
250
|
+
organism: Species (default: "Homo sapiens"). Also: "Mus musculus"
|
|
251
|
+
organ: Organ/tissue system (e.g., "pancreas", "brain", "liver")
|
|
252
|
+
biosample_type: Sample classification ("tissue", "cell line", "primary cell", "organoid")
|
|
253
|
+
biosample_term_name: Specific cell/tissue name (e.g., "GM12878", "HepG2", "pancreas")
|
|
254
|
+
target: ChIP/CUT&RUN target (e.g., "H3K27me3", "CTCF", "p300")
|
|
255
|
+
status: Data status (default: "released"). Also: "archived", "revoked"
|
|
256
|
+
lab: Submitting lab name
|
|
257
|
+
award: Funding project
|
|
258
|
+
assembly: Genome assembly (e.g., "GRCh38", "mm10")
|
|
259
|
+
replication_type: "isogenic", "anisogenic", or "unreplicated"
|
|
260
|
+
life_stage: "embryonic", "postnatal", "child", "adult"
|
|
261
|
+
sex: "male", "female", "mixed"
|
|
262
|
+
treatment: Treatment name if perturbation experiment
|
|
263
|
+
genetic_modification: Modification type ("CRISPR", "RNAi")
|
|
264
|
+
perturbed: True for perturbation experiments only
|
|
265
|
+
search_term: Free text search across all fields
|
|
266
|
+
date_released_from: Start date (YYYY-MM-DD) for date range filter
|
|
267
|
+
date_released_to: End date (YYYY-MM-DD) for date range filter
|
|
268
|
+
limit: Max results to return (default 25, use larger for comprehensive searches)
|
|
269
|
+
offset: Skip first N results (for pagination)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
JSON with experiment results, total count, and pagination info.
|
|
273
|
+
"""
|
|
274
|
+
client = await _get_client()
|
|
275
|
+
limit = clamp_limit(limit)
|
|
276
|
+
|
|
277
|
+
filter_warnings = _validate_filters(assay_title, organ, biosample_type)
|
|
278
|
+
|
|
279
|
+
result = await client.search_experiments(
|
|
280
|
+
assay_title=assay_title,
|
|
281
|
+
organism=organism,
|
|
282
|
+
organ=organ,
|
|
283
|
+
biosample_type=biosample_type,
|
|
284
|
+
biosample_term_name=biosample_term_name,
|
|
285
|
+
target=target,
|
|
286
|
+
status=status,
|
|
287
|
+
lab=lab,
|
|
288
|
+
award=award,
|
|
289
|
+
assembly=assembly,
|
|
290
|
+
replication_type=replication_type,
|
|
291
|
+
life_stage=life_stage,
|
|
292
|
+
sex=sex,
|
|
293
|
+
treatment=treatment,
|
|
294
|
+
genetic_modification=genetic_modification,
|
|
295
|
+
perturbed=perturbed,
|
|
296
|
+
search_term=search_term,
|
|
297
|
+
date_released_from=date_released_from,
|
|
298
|
+
date_released_to=date_released_to,
|
|
299
|
+
limit=limit,
|
|
300
|
+
offset=offset,
|
|
301
|
+
)
|
|
302
|
+
total = result.get("total", 0)
|
|
303
|
+
serialized = _serialize(result)
|
|
304
|
+
serialized["has_more"] = total > (offset + limit)
|
|
305
|
+
serialized["next_offset"] = offset + limit if total > (offset + limit) else None
|
|
306
|
+
if filter_warnings:
|
|
307
|
+
serialized["filter_warnings"] = filter_warnings
|
|
308
|
+
if not result.get("results"):
|
|
309
|
+
serialized["suggestion"] = (
|
|
310
|
+
"Try broadening your search filters. Use encode_get_facets to see what data is available for your criteria."
|
|
311
|
+
)
|
|
312
|
+
return json.dumps(serialized, indent=2)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ======================================================================
|
|
316
|
+
# Tool 2: Get Experiment Details
|
|
317
|
+
# ======================================================================
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@mcp.tool(annotations=_READONLY_API, title="Get Experiment Details")
|
|
321
|
+
async def encode_get_experiment(accession: str) -> str:
|
|
322
|
+
"""Get full details for a specific ENCODE experiment by accession ID.
|
|
323
|
+
|
|
324
|
+
Returns complete experiment metadata including all associated files,
|
|
325
|
+
quality metrics, controls, replicate information, and audit status.
|
|
326
|
+
|
|
327
|
+
WHEN TO USE: Use when you have a specific accession and need full details
|
|
328
|
+
including files, quality metrics, and audit status.
|
|
329
|
+
RELATED TOOLS: encode_list_files, encode_track_experiment, encode_compare_experiments
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
accession: ENCODE experiment accession (e.g., "ENCSR133RZO", "ENCSR000AKS")
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
JSON with full experiment details and file listing.
|
|
336
|
+
"""
|
|
337
|
+
validate_accession(accession)
|
|
338
|
+
client = await _get_client()
|
|
339
|
+
result = await client.get_experiment(accession)
|
|
340
|
+
return json.dumps(_serialize(result), indent=2)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ======================================================================
|
|
344
|
+
# Tool 3: List Files for Experiment
|
|
345
|
+
# ======================================================================
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@mcp.tool(annotations=_READONLY_API, title="List Experiment Files")
|
|
349
|
+
async def encode_list_files(
|
|
350
|
+
experiment_accession: str,
|
|
351
|
+
file_format: str | None = None,
|
|
352
|
+
file_type: str | None = None,
|
|
353
|
+
output_type: str | None = None,
|
|
354
|
+
output_category: str | None = None,
|
|
355
|
+
assembly: str | None = None,
|
|
356
|
+
status: str | None = None,
|
|
357
|
+
preferred_default: bool | None = None,
|
|
358
|
+
limit: int = 200,
|
|
359
|
+
) -> str:
|
|
360
|
+
"""List all files for a specific ENCODE experiment, with optional filters.
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
- All BED files: experiment_accession="ENCSR133RZO", file_format="bed"
|
|
364
|
+
- FASTQs only: experiment_accession="ENCSR133RZO", file_format="fastq"
|
|
365
|
+
- Signal tracks: experiment_accession="ENCSR133RZO", output_category="signal"
|
|
366
|
+
- Default/recommended files: preferred_default=True
|
|
367
|
+
- Peaks from GRCh38: file_format="bed", output_type="IDR thresholded peaks", assembly="GRCh38"
|
|
368
|
+
|
|
369
|
+
Common file_format values: "fastq", "bam", "bed", "bigWig", "bigBed", "tsv", "hic"
|
|
370
|
+
|
|
371
|
+
Common output_type values: "reads", "alignments", "signal of unique reads",
|
|
372
|
+
"signal of all reads", "fold change over control", "IDR thresholded peaks",
|
|
373
|
+
"pseudoreplicated peaks", "replicated peaks", "gene quantifications",
|
|
374
|
+
"transcript quantifications", "contact matrix"
|
|
375
|
+
|
|
376
|
+
WHEN TO USE: Use to browse files within a known experiment. Use encode_search_files
|
|
377
|
+
instead to find files across experiments.
|
|
378
|
+
RELATED TOOLS: encode_search_files, encode_get_file_info, encode_download_files
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
experiment_accession: ENCODE experiment accession (e.g., "ENCSR133RZO")
|
|
382
|
+
file_format: Filter by format ("fastq", "bam", "bed", "bigWig", "bigBed", etc.)
|
|
383
|
+
file_type: Filter by specific type ("bed narrowPeak", "bed broadPeak", etc.)
|
|
384
|
+
output_type: Filter by output type ("reads", "peaks", "signal", etc.)
|
|
385
|
+
output_category: Filter by category ("raw data", "alignment", "signal", "annotation")
|
|
386
|
+
assembly: Filter by genome assembly ("GRCh38", "hg19", "mm10")
|
|
387
|
+
status: Filter by status ("released", "archived", "in progress")
|
|
388
|
+
preferred_default: If True, return only default/recommended files
|
|
389
|
+
limit: Max files to return (default 200)
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
JSON list of files with accession, format, size, download URL, and metadata.
|
|
393
|
+
"""
|
|
394
|
+
validate_accession(experiment_accession)
|
|
395
|
+
limit = clamp_limit(limit)
|
|
396
|
+
client = await _get_client()
|
|
397
|
+
results = await client.list_files(
|
|
398
|
+
experiment_accession=experiment_accession,
|
|
399
|
+
file_format=file_format,
|
|
400
|
+
file_type=file_type,
|
|
401
|
+
output_type=output_type,
|
|
402
|
+
output_category=output_category,
|
|
403
|
+
assembly=assembly,
|
|
404
|
+
status=status,
|
|
405
|
+
preferred_default=preferred_default,
|
|
406
|
+
limit=limit,
|
|
407
|
+
)
|
|
408
|
+
return json.dumps(_serialize(results), indent=2)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ======================================================================
|
|
412
|
+
# Tool 4: Search Files Across Experiments
|
|
413
|
+
# ======================================================================
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@mcp.tool(annotations=_READONLY_API, title="Search Files Across Experiments")
|
|
417
|
+
async def encode_search_files(
|
|
418
|
+
file_format: str | None = None,
|
|
419
|
+
file_type: str | None = None,
|
|
420
|
+
output_type: str | None = None,
|
|
421
|
+
output_category: str | None = None,
|
|
422
|
+
assembly: str | None = None,
|
|
423
|
+
assay_title: str | None = None,
|
|
424
|
+
organism: str | None = None,
|
|
425
|
+
organ: str | None = None,
|
|
426
|
+
biosample_type: str | None = None,
|
|
427
|
+
target: str | None = None,
|
|
428
|
+
status: str = "released",
|
|
429
|
+
preferred_default: bool | None = None,
|
|
430
|
+
search_term: str | None = None,
|
|
431
|
+
limit: int = 25,
|
|
432
|
+
offset: int = 0,
|
|
433
|
+
) -> str:
|
|
434
|
+
"""Search files across ALL experiments with combined experiment + file filters.
|
|
435
|
+
|
|
436
|
+
This is powerful for finding specific file types across many experiments.
|
|
437
|
+
|
|
438
|
+
Examples:
|
|
439
|
+
- All BED files from human pancreas ChIP-seq:
|
|
440
|
+
file_format="bed", assay_title="Histone ChIP-seq", organ="pancreas"
|
|
441
|
+
- FASTQs from mouse liver RNA-seq:
|
|
442
|
+
file_format="fastq", assay_title="total RNA-seq", organ="liver", organism="Mus musculus"
|
|
443
|
+
- All IDR peak files for H3K27me3:
|
|
444
|
+
output_type="IDR thresholded peaks", target="H3K27me3"
|
|
445
|
+
- BigWig signal tracks from ATAC-seq on brain tissue:
|
|
446
|
+
file_format="bigWig", assay_title="ATAC-seq", organ="brain", biosample_type="tissue"
|
|
447
|
+
|
|
448
|
+
WHEN TO USE: Use to find specific file types across ALL experiments. More powerful
|
|
449
|
+
than encode_list_files for cross-experiment file discovery.
|
|
450
|
+
RELATED TOOLS: encode_list_files, encode_batch_download, encode_get_file_info
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
file_format: File format ("fastq", "bam", "bed", "bigWig", etc.)
|
|
454
|
+
file_type: Specific file type ("bed narrowPeak", "bed broadPeak", etc.)
|
|
455
|
+
output_type: Output type ("reads", "peaks", "signal", etc.)
|
|
456
|
+
output_category: Output category ("raw data", "alignment", "signal", "annotation")
|
|
457
|
+
assembly: Genome assembly ("GRCh38", "hg19", "mm10")
|
|
458
|
+
assay_title: Filter by assay type of parent experiment
|
|
459
|
+
organism: Filter by organism of parent experiment
|
|
460
|
+
organ: Filter by organ of parent experiment
|
|
461
|
+
biosample_type: Filter by biosample type ("tissue", "cell line", etc.)
|
|
462
|
+
target: Filter by ChIP/CUT&RUN target
|
|
463
|
+
status: File status (default: "released")
|
|
464
|
+
preferred_default: If True, only default/recommended files
|
|
465
|
+
search_term: Free text search
|
|
466
|
+
limit: Max results (default 25)
|
|
467
|
+
offset: Skip first N results (pagination)
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
JSON with file results, total count, and pagination info.
|
|
471
|
+
"""
|
|
472
|
+
client = await _get_client()
|
|
473
|
+
limit = clamp_limit(limit)
|
|
474
|
+
filter_warnings = _validate_filters(assay_title, organ, biosample_type)
|
|
475
|
+
result = await client.search_files(
|
|
476
|
+
file_format=file_format,
|
|
477
|
+
file_type=file_type,
|
|
478
|
+
output_type=output_type,
|
|
479
|
+
output_category=output_category,
|
|
480
|
+
assembly=assembly,
|
|
481
|
+
assay_title=assay_title,
|
|
482
|
+
organism=organism,
|
|
483
|
+
organ=organ,
|
|
484
|
+
biosample_type=biosample_type,
|
|
485
|
+
target=target,
|
|
486
|
+
status=status,
|
|
487
|
+
preferred_default=preferred_default,
|
|
488
|
+
search_term=search_term,
|
|
489
|
+
limit=limit,
|
|
490
|
+
offset=offset,
|
|
491
|
+
)
|
|
492
|
+
total = result.get("total", 0)
|
|
493
|
+
serialized = _serialize(result)
|
|
494
|
+
serialized["has_more"] = total > (offset + limit)
|
|
495
|
+
serialized["next_offset"] = offset + limit if total > (offset + limit) else None
|
|
496
|
+
if filter_warnings:
|
|
497
|
+
serialized["filter_warnings"] = filter_warnings
|
|
498
|
+
if not result.get("results"):
|
|
499
|
+
serialized["suggestion"] = (
|
|
500
|
+
"Verify assembly and file_format values. Use encode_get_metadata('file_formats') to see valid options."
|
|
501
|
+
)
|
|
502
|
+
return json.dumps(serialized, indent=2)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ======================================================================
|
|
506
|
+
# Tool 5: Download Files
|
|
507
|
+
# ======================================================================
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@mcp.tool(annotations=_DOWNLOAD, title="Download ENCODE Files")
|
|
511
|
+
async def encode_download_files(
|
|
512
|
+
file_accessions: list[str],
|
|
513
|
+
download_dir: str,
|
|
514
|
+
organize_by: Literal["flat", "experiment", "format", "experiment_format"] = "flat",
|
|
515
|
+
verify_md5: bool = True,
|
|
516
|
+
) -> str:
|
|
517
|
+
"""Download specific ENCODE files by accession to a local directory.
|
|
518
|
+
|
|
519
|
+
Downloads files from ENCODE to your local machine. Supports MD5 verification,
|
|
520
|
+
concurrent downloads, and skip-if-already-downloaded.
|
|
521
|
+
|
|
522
|
+
WHEN TO USE: Use for downloading specific files by accession. For bulk downloads,
|
|
523
|
+
prefer encode_batch_download.
|
|
524
|
+
RELATED TOOLS: encode_batch_download, encode_search_files, encode_log_derived_file
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
file_accessions: List of file accessions to download (e.g., ["ENCFF635JIA", "ENCFF388RZD"])
|
|
528
|
+
download_dir: Local directory path to save files (e.g., "./data/encode")
|
|
529
|
+
organize_by: How to organize downloaded files:
|
|
530
|
+
- "flat": All files in download_dir (default)
|
|
531
|
+
- "experiment": download_dir/ENCSR.../filename
|
|
532
|
+
- "format": download_dir/bed/filename
|
|
533
|
+
- "experiment_format": download_dir/ENCSR.../bed/filename
|
|
534
|
+
verify_md5: Verify file integrity with MD5 checksum (default True)
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
JSON with download results for each file (path, size, success/error, MD5 status).
|
|
538
|
+
"""
|
|
539
|
+
validate_organize_by(organize_by)
|
|
540
|
+
for acc in file_accessions:
|
|
541
|
+
validate_accession(acc)
|
|
542
|
+
|
|
543
|
+
client = await _get_client()
|
|
544
|
+
downloader = _get_downloader()
|
|
545
|
+
|
|
546
|
+
# Get file info for each accession
|
|
547
|
+
file_infos = []
|
|
548
|
+
errors = []
|
|
549
|
+
for acc in file_accessions:
|
|
550
|
+
try:
|
|
551
|
+
info = await client.get_file_info(acc)
|
|
552
|
+
file_infos.append(info)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
errors.append({"accession": acc, "error": str(e)})
|
|
555
|
+
|
|
556
|
+
# Download all files
|
|
557
|
+
results = await downloader.download_batch(file_infos, download_dir, organize_by, verify_md5)
|
|
558
|
+
|
|
559
|
+
output = {
|
|
560
|
+
"downloaded": _serialize(results),
|
|
561
|
+
"errors": errors,
|
|
562
|
+
"summary": {
|
|
563
|
+
"total_requested": len(file_accessions),
|
|
564
|
+
"successful": sum(1 for r in results if r.success),
|
|
565
|
+
"failed": sum(1 for r in results if not r.success) + len(errors),
|
|
566
|
+
"total_size": sum(r.file_size for r in results if r.success),
|
|
567
|
+
"total_size_human": _human_size(sum(r.file_size for r in results if r.success)),
|
|
568
|
+
},
|
|
569
|
+
}
|
|
570
|
+
return json.dumps(output, indent=2)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# ======================================================================
|
|
574
|
+
# Tool 6: Get Metadata / Filter Values
|
|
575
|
+
# ======================================================================
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@mcp.tool(annotations=_READONLY_API, title="Get Filter Values")
|
|
579
|
+
async def encode_get_metadata(
|
|
580
|
+
metadata_type: Literal[
|
|
581
|
+
"assays",
|
|
582
|
+
"organisms",
|
|
583
|
+
"organs",
|
|
584
|
+
"biosample_types",
|
|
585
|
+
"file_formats",
|
|
586
|
+
"output_types",
|
|
587
|
+
"output_categories",
|
|
588
|
+
"assemblies",
|
|
589
|
+
"life_stages",
|
|
590
|
+
"replication_types",
|
|
591
|
+
"statuses",
|
|
592
|
+
"file_statuses",
|
|
593
|
+
],
|
|
594
|
+
) -> str:
|
|
595
|
+
"""Get available filter values for ENCODE searches.
|
|
596
|
+
|
|
597
|
+
Use this to discover valid values for search parameters.
|
|
598
|
+
|
|
599
|
+
WHEN TO USE: Use to discover valid filter values before searching. Helps prevent
|
|
600
|
+
typos in assay_title, organ, biosample_type etc.
|
|
601
|
+
RELATED TOOLS: encode_get_facets, encode_search_experiments
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
metadata_type: Type of metadata to retrieve. Options:
|
|
605
|
+
- "assays": Available assay types (Histone ChIP-seq, ATAC-seq, total RNA-seq, etc.)
|
|
606
|
+
- "organisms": Available organisms (Homo sapiens, Mus musculus, etc.)
|
|
607
|
+
- "organs": Available organ/tissue systems (pancreas, brain, liver, etc.)
|
|
608
|
+
- "biosample_types": Biosample classifications (tissue, cell line, primary cell, etc.)
|
|
609
|
+
- "file_formats": File format types (fastq, bam, bed, bigWig, etc.)
|
|
610
|
+
- "output_types": Output data types (reads, peaks, signal, etc.)
|
|
611
|
+
- "output_categories": Output categories (raw data, alignment, signal, etc.)
|
|
612
|
+
- "assemblies": Genome assemblies (GRCh38, hg19, mm10, etc.)
|
|
613
|
+
- "life_stages": Life stages (embryonic, adult, child, etc.)
|
|
614
|
+
- "replication_types": Replication types (isogenic, anisogenic, unreplicated)
|
|
615
|
+
- "statuses": Experiment statuses (released, archived, etc.)
|
|
616
|
+
- "file_statuses": File statuses (released, archived, in progress, etc.)
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
JSON list of valid values for the specified metadata type.
|
|
620
|
+
"""
|
|
621
|
+
client = await _get_client()
|
|
622
|
+
try:
|
|
623
|
+
values = client.get_metadata(metadata_type)
|
|
624
|
+
return json.dumps({"metadata_type": metadata_type, "values": values, "count": len(values)}, indent=2)
|
|
625
|
+
except ValueError as e:
|
|
626
|
+
return json.dumps({"error": str(e)}, indent=2)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# ======================================================================
|
|
630
|
+
# Tool 7: Batch Download from Search
|
|
631
|
+
# ======================================================================
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@mcp.tool(annotations=_DOWNLOAD, title="Batch Search and Download")
|
|
635
|
+
async def encode_batch_download(
|
|
636
|
+
download_dir: str,
|
|
637
|
+
file_format: str | None = None,
|
|
638
|
+
output_type: str | None = None,
|
|
639
|
+
output_category: str | None = None,
|
|
640
|
+
assembly: str | None = None,
|
|
641
|
+
assay_title: str | None = None,
|
|
642
|
+
organism: str = "Homo sapiens",
|
|
643
|
+
organ: str | None = None,
|
|
644
|
+
biosample_type: str | None = None,
|
|
645
|
+
target: str | None = None,
|
|
646
|
+
preferred_default: bool | None = None,
|
|
647
|
+
organize_by: Literal["flat", "experiment", "format", "experiment_format"] = "experiment",
|
|
648
|
+
verify_md5: bool = True,
|
|
649
|
+
limit: int = 100,
|
|
650
|
+
dry_run: bool = True,
|
|
651
|
+
) -> str:
|
|
652
|
+
"""Search for files and download them all in batch.
|
|
653
|
+
|
|
654
|
+
First searches for files matching the criteria, then downloads them.
|
|
655
|
+
By default runs in dry_run mode to preview what would be downloaded.
|
|
656
|
+
Set dry_run=False to actually download.
|
|
657
|
+
|
|
658
|
+
WHEN TO USE: Use for searching and downloading files in one step. Always use
|
|
659
|
+
dry_run=True first to preview. For specific file accessions, use encode_download_files.
|
|
660
|
+
RELATED TOOLS: encode_download_files, encode_search_files
|
|
661
|
+
|
|
662
|
+
Examples:
|
|
663
|
+
- Download all BED files from human pancreas ChIP-seq:
|
|
664
|
+
file_format="bed", assay_title="Histone ChIP-seq", organ="pancreas",
|
|
665
|
+
download_dir="/data/encode", dry_run=False
|
|
666
|
+
- Preview FASTQ downloads for mouse brain RNA-seq:
|
|
667
|
+
file_format="fastq", assay_title="total RNA-seq", organ="brain",
|
|
668
|
+
organism="Mus musculus", download_dir="/data/encode"
|
|
669
|
+
- Download IDR peaks for H3K27me3 in GRCh38:
|
|
670
|
+
output_type="IDR thresholded peaks", target="H3K27me3", assembly="GRCh38",
|
|
671
|
+
download_dir="/data/encode", dry_run=False
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
download_dir: Local directory to save files
|
|
675
|
+
file_format: File format filter ("fastq", "bam", "bed", "bigWig", etc.)
|
|
676
|
+
output_type: Output type filter ("reads", "peaks", "signal", etc.)
|
|
677
|
+
output_category: Output category ("raw data", "alignment", "annotation", etc.)
|
|
678
|
+
assembly: Genome assembly ("GRCh38", "mm10", etc.)
|
|
679
|
+
assay_title: Assay type ("Histone ChIP-seq", "ATAC-seq", "total RNA-seq", etc.)
|
|
680
|
+
organism: Organism (default: "Homo sapiens")
|
|
681
|
+
organ: Organ/tissue ("pancreas", "brain", "liver", etc.)
|
|
682
|
+
biosample_type: Biosample type ("tissue", "cell line", "primary cell", etc.)
|
|
683
|
+
target: ChIP/CUT&RUN target ("H3K27me3", "CTCF", etc.)
|
|
684
|
+
preferred_default: If True, only download default/recommended files
|
|
685
|
+
organize_by: File organization ("flat", "experiment", "format", "experiment_format")
|
|
686
|
+
verify_md5: Verify downloads with MD5 checksums (default True)
|
|
687
|
+
limit: Max files to download (default 100, safety limit)
|
|
688
|
+
dry_run: If True (default), only preview what would be downloaded. Set False to download.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
JSON with download preview (dry_run=True) or download results (dry_run=False).
|
|
692
|
+
"""
|
|
693
|
+
client = await _get_client()
|
|
694
|
+
downloader = _get_downloader()
|
|
695
|
+
validate_organize_by(organize_by)
|
|
696
|
+
limit = clamp_limit(limit)
|
|
697
|
+
filter_warnings = _validate_filters(assay_title, organ, biosample_type)
|
|
698
|
+
|
|
699
|
+
# Search for files
|
|
700
|
+
search_result = await client.search_files(
|
|
701
|
+
file_format=file_format,
|
|
702
|
+
output_type=output_type,
|
|
703
|
+
output_category=output_category,
|
|
704
|
+
assembly=assembly,
|
|
705
|
+
assay_title=assay_title,
|
|
706
|
+
organism=organism,
|
|
707
|
+
organ=organ,
|
|
708
|
+
biosample_type=biosample_type,
|
|
709
|
+
target=target,
|
|
710
|
+
status="released",
|
|
711
|
+
preferred_default=preferred_default,
|
|
712
|
+
limit=limit,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
files = search_result["results"]
|
|
716
|
+
|
|
717
|
+
if not files:
|
|
718
|
+
empty_result = {
|
|
719
|
+
"message": "No files found matching the search criteria.",
|
|
720
|
+
"total": 0,
|
|
721
|
+
"has_more": False,
|
|
722
|
+
"next_offset": None,
|
|
723
|
+
"suggestion": "Try broadening your search filters. Use encode_get_facets to see what data is available for your criteria.",
|
|
724
|
+
}
|
|
725
|
+
if filter_warnings:
|
|
726
|
+
empty_result["filter_warnings"] = filter_warnings
|
|
727
|
+
return json.dumps(empty_result, indent=2)
|
|
728
|
+
|
|
729
|
+
if dry_run:
|
|
730
|
+
# Preview mode
|
|
731
|
+
search_total = search_result["total"]
|
|
732
|
+
preview = downloader.preview_downloads(files, download_dir, organize_by)
|
|
733
|
+
preview["message"] = (
|
|
734
|
+
f"Found {preview['file_count']} files ({preview['total_size_human']}). Set dry_run=False to download."
|
|
735
|
+
)
|
|
736
|
+
preview["search_total"] = search_total
|
|
737
|
+
preview["has_more"] = search_total > limit
|
|
738
|
+
preview["next_offset"] = limit if search_total > limit else None
|
|
739
|
+
if filter_warnings:
|
|
740
|
+
preview["filter_warnings"] = filter_warnings
|
|
741
|
+
return json.dumps(_serialize(preview), indent=2)
|
|
742
|
+
|
|
743
|
+
# Actually download
|
|
744
|
+
results = await downloader.download_batch(files, download_dir, organize_by, verify_md5)
|
|
745
|
+
search_total = search_result["total"]
|
|
746
|
+
|
|
747
|
+
output = {
|
|
748
|
+
"downloaded": _serialize(results),
|
|
749
|
+
"summary": {
|
|
750
|
+
"total_found": search_total,
|
|
751
|
+
"total_downloaded": len(results),
|
|
752
|
+
"successful": sum(1 for r in results if r.success),
|
|
753
|
+
"failed": sum(1 for r in results if not r.success),
|
|
754
|
+
"total_size": sum(r.file_size for r in results if r.success),
|
|
755
|
+
"total_size_human": _human_size(sum(r.file_size for r in results if r.success)),
|
|
756
|
+
},
|
|
757
|
+
"has_more": search_total > limit,
|
|
758
|
+
"next_offset": limit if search_total > limit else None,
|
|
759
|
+
}
|
|
760
|
+
if filter_warnings:
|
|
761
|
+
output["filter_warnings"] = filter_warnings
|
|
762
|
+
return json.dumps(output, indent=2)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
# ======================================================================
|
|
766
|
+
# Tool 8: Store/Manage Credentials
|
|
767
|
+
# ======================================================================
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
@mcp.tool(annotations=_CREDENTIAL_MGMT, title="Manage API Credentials")
|
|
771
|
+
async def encode_manage_credentials(
|
|
772
|
+
action: Literal["store", "check", "clear"],
|
|
773
|
+
access_key: str | None = None,
|
|
774
|
+
secret_key: str | None = None,
|
|
775
|
+
) -> str:
|
|
776
|
+
"""Manage ENCODE API credentials for accessing restricted/unreleased data.
|
|
777
|
+
|
|
778
|
+
Most ENCODE data is public and requires no authentication.
|
|
779
|
+
Credentials are only needed for unreleased or restricted datasets.
|
|
780
|
+
|
|
781
|
+
Credentials are stored securely in your OS keyring (macOS Keychain,
|
|
782
|
+
Linux Secret Service, Windows Credential Locker) and never in plaintext.
|
|
783
|
+
|
|
784
|
+
WHEN TO USE: Use only for accessing unreleased/restricted ENCODE data.
|
|
785
|
+
Public data requires no authentication.
|
|
786
|
+
RELATED TOOLS: encode_search_experiments
|
|
787
|
+
|
|
788
|
+
Args:
|
|
789
|
+
action: What to do:
|
|
790
|
+
- "store": Save new credentials (requires access_key and secret_key)
|
|
791
|
+
- "check": Check if credentials are configured
|
|
792
|
+
- "clear": Remove stored credentials
|
|
793
|
+
access_key: Your ENCODE access key (only for action="store")
|
|
794
|
+
secret_key: Your ENCODE secret key (only for action="store")
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
JSON with action result.
|
|
798
|
+
"""
|
|
799
|
+
if action == "store":
|
|
800
|
+
if not access_key or not secret_key:
|
|
801
|
+
return json.dumps(
|
|
802
|
+
{
|
|
803
|
+
"error": "Both access_key and secret_key are required to store credentials.",
|
|
804
|
+
"help": "Get your access key pair from your ENCODE profile at https://www.encodeproject.org/",
|
|
805
|
+
},
|
|
806
|
+
indent=2,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
location = _credential_manager.store_credentials(access_key, secret_key)
|
|
810
|
+
# Reset client to pick up new credentials
|
|
811
|
+
global _client
|
|
812
|
+
async with _get_client_lock():
|
|
813
|
+
if _client:
|
|
814
|
+
await _client.close()
|
|
815
|
+
_client = EncodeClient(credential_manager=_credential_manager)
|
|
816
|
+
|
|
817
|
+
return json.dumps(
|
|
818
|
+
{
|
|
819
|
+
"success": True,
|
|
820
|
+
"message": f"Credentials stored securely in: {location}",
|
|
821
|
+
"note": "Credentials are encrypted and never stored in plaintext.",
|
|
822
|
+
},
|
|
823
|
+
indent=2,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
elif action == "check":
|
|
827
|
+
has_creds = _credential_manager.has_credentials
|
|
828
|
+
return json.dumps(
|
|
829
|
+
{
|
|
830
|
+
"credentials_configured": has_creds,
|
|
831
|
+
"message": (
|
|
832
|
+
"Credentials are configured. You can access restricted data."
|
|
833
|
+
if has_creds
|
|
834
|
+
else "No credentials configured. You can still access all public ENCODE data. "
|
|
835
|
+
"Use action='store' with your ENCODE access key pair to access restricted data."
|
|
836
|
+
),
|
|
837
|
+
},
|
|
838
|
+
indent=2,
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
elif action == "clear":
|
|
842
|
+
_credential_manager.clear_credentials()
|
|
843
|
+
# Reset client
|
|
844
|
+
async with _get_client_lock():
|
|
845
|
+
if _client: # type: ignore[used-before-def]
|
|
846
|
+
await _client.close() # type: ignore[used-before-def]
|
|
847
|
+
_client = EncodeClient(credential_manager=_credential_manager)
|
|
848
|
+
|
|
849
|
+
return json.dumps(
|
|
850
|
+
{
|
|
851
|
+
"success": True,
|
|
852
|
+
"message": "All stored credentials have been removed.",
|
|
853
|
+
},
|
|
854
|
+
indent=2,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
else:
|
|
858
|
+
return json.dumps(
|
|
859
|
+
{
|
|
860
|
+
"error": f"Unknown action: {action}. Use 'store', 'check', or 'clear'.",
|
|
861
|
+
},
|
|
862
|
+
indent=2,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# ======================================================================
|
|
867
|
+
# Tool 9: Get Live Facets (Dynamic Filter Discovery)
|
|
868
|
+
# ======================================================================
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@mcp.tool(annotations=_READONLY_API, title="Explore Available Data")
|
|
872
|
+
async def encode_get_facets(
|
|
873
|
+
search_type: str = "Experiment",
|
|
874
|
+
assay_title: str | None = None,
|
|
875
|
+
organism: str | None = None,
|
|
876
|
+
organ: str | None = None,
|
|
877
|
+
biosample_type: str | None = None,
|
|
878
|
+
) -> str:
|
|
879
|
+
"""Get live filter counts from ENCODE to discover what data is available.
|
|
880
|
+
|
|
881
|
+
Returns faceted counts showing how many experiments/files exist for each
|
|
882
|
+
filter value. Useful for exploring what's available before searching.
|
|
883
|
+
|
|
884
|
+
WHEN TO USE: Use to explore what data exists before searching. Shows counts
|
|
885
|
+
per filter value. Best first step for unknown datasets.
|
|
886
|
+
RELATED TOOLS: encode_get_metadata, encode_search_experiments
|
|
887
|
+
|
|
888
|
+
Examples:
|
|
889
|
+
- What assays are available for pancreas?
|
|
890
|
+
organ="pancreas"
|
|
891
|
+
- What organs have Histone ChIP-seq data?
|
|
892
|
+
assay_title="Histone ChIP-seq"
|
|
893
|
+
- What targets are available for mouse brain ChIP-seq?
|
|
894
|
+
assay_title="Histone ChIP-seq", organism="Mus musculus", organ="brain"
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
search_type: Object type ("Experiment" or "File")
|
|
898
|
+
assay_title: Pre-filter by assay type
|
|
899
|
+
organism: Pre-filter by organism
|
|
900
|
+
organ: Pre-filter by organ
|
|
901
|
+
biosample_type: Pre-filter by biosample type
|
|
902
|
+
|
|
903
|
+
Returns:
|
|
904
|
+
JSON with facet names and their term counts.
|
|
905
|
+
"""
|
|
906
|
+
client = await _get_client()
|
|
907
|
+
filter_warnings = _validate_filters(assay_title, organ, biosample_type)
|
|
908
|
+
filters = {}
|
|
909
|
+
if assay_title:
|
|
910
|
+
filters["assay_title"] = assay_title
|
|
911
|
+
if organism:
|
|
912
|
+
filters["replicates.library.biosample.donor.organism.scientific_name"] = organism
|
|
913
|
+
if organ:
|
|
914
|
+
filters["biosample_ontology.organ_slims"] = organ
|
|
915
|
+
if biosample_type:
|
|
916
|
+
filters["biosample_ontology.classification"] = biosample_type
|
|
917
|
+
|
|
918
|
+
facets = await client.search_facets(search_type=search_type, **filters)
|
|
919
|
+
|
|
920
|
+
# Simplify output - show most useful facets
|
|
921
|
+
useful_facets = {}
|
|
922
|
+
for field, terms in facets.items():
|
|
923
|
+
# Only include facets with reasonable number of terms
|
|
924
|
+
if len(terms) <= 200:
|
|
925
|
+
useful_facets[field] = terms[:50] # Cap at 50 terms per facet
|
|
926
|
+
|
|
927
|
+
result: dict = dict(useful_facets)
|
|
928
|
+
if filter_warnings:
|
|
929
|
+
result["filter_warnings"] = filter_warnings
|
|
930
|
+
return json.dumps(result, indent=2)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# ======================================================================
|
|
934
|
+
# Tool 10: Get File Info
|
|
935
|
+
# ======================================================================
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@mcp.tool(annotations=_READONLY_API, title="Get File Details")
|
|
939
|
+
async def encode_get_file_info(accession: str) -> str:
|
|
940
|
+
"""Get detailed information about a specific ENCODE file.
|
|
941
|
+
|
|
942
|
+
WHEN TO USE: Use when you need detailed metadata for a specific file
|
|
943
|
+
(size, md5, assembly, biological replicate info).
|
|
944
|
+
RELATED TOOLS: encode_download_files, encode_list_files
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
accession: File accession ID (e.g., "ENCFF635JIA")
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
JSON with file metadata including format, size, download URL, MD5, assembly, etc.
|
|
951
|
+
"""
|
|
952
|
+
validate_accession(accession)
|
|
953
|
+
client = await _get_client()
|
|
954
|
+
info = await client.get_file_info(accession)
|
|
955
|
+
return json.dumps(_serialize(info), indent=2)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
# ======================================================================
|
|
959
|
+
# Tracker helper
|
|
960
|
+
# ======================================================================
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _get_tracker() -> ExperimentTracker:
|
|
964
|
+
if _tracker is None:
|
|
965
|
+
raise RuntimeError("Experiment tracker not initialized")
|
|
966
|
+
return _tracker
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
# ======================================================================
|
|
970
|
+
# Tool 11: Track Experiment
|
|
971
|
+
# ======================================================================
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@mcp.tool(annotations=_WRITE_LOCAL_API, title="Track Experiment Locally")
|
|
975
|
+
async def encode_track_experiment(
|
|
976
|
+
accession: str,
|
|
977
|
+
fetch_publications: bool = True,
|
|
978
|
+
fetch_pipelines: bool = True,
|
|
979
|
+
notes: str = "",
|
|
980
|
+
) -> str:
|
|
981
|
+
"""Track an ENCODE experiment locally with its publications, methods, and pipeline info.
|
|
982
|
+
|
|
983
|
+
Fetches full experiment metadata from ENCODE and stores it in a local SQLite
|
|
984
|
+
database along with any associated publications (PMIDs, DOIs, authors, journal)
|
|
985
|
+
and pipeline/analysis information (software versions, methods).
|
|
986
|
+
|
|
987
|
+
This is like adding an experiment to your "library" - similar to Endnote for papers.
|
|
988
|
+
|
|
989
|
+
WHEN TO USE: Use to save an experiment to your local library with publications
|
|
990
|
+
and pipeline info. Required before compare or citations.
|
|
991
|
+
RELATED TOOLS: encode_compare_experiments, encode_get_citations, encode_export_data
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
accession: ENCODE experiment accession (e.g., "ENCSR133RZO")
|
|
995
|
+
fetch_publications: Also fetch and store publications/citations (default True)
|
|
996
|
+
fetch_pipelines: Also fetch and store pipeline/analysis info (default True)
|
|
997
|
+
notes: Optional notes to attach to this experiment
|
|
998
|
+
|
|
999
|
+
Returns:
|
|
1000
|
+
JSON with tracking result including publications and pipeline info found.
|
|
1001
|
+
"""
|
|
1002
|
+
client = await _get_client()
|
|
1003
|
+
tracker = _get_tracker()
|
|
1004
|
+
validate_accession(accession)
|
|
1005
|
+
|
|
1006
|
+
# Get full experiment data from ENCODE
|
|
1007
|
+
exp_data = await client.get_experiment_raw(accession)
|
|
1008
|
+
|
|
1009
|
+
# Track the experiment
|
|
1010
|
+
from encode_connector.client.models import ExperimentDetail
|
|
1011
|
+
|
|
1012
|
+
detail = ExperimentDetail.from_api(exp_data)
|
|
1013
|
+
result = tracker.track_experiment(detail.model_dump(), raw_metadata=exp_data)
|
|
1014
|
+
|
|
1015
|
+
if notes:
|
|
1016
|
+
tracker.add_note(accession, notes)
|
|
1017
|
+
|
|
1018
|
+
output: dict[str, Any] = {"tracking": result}
|
|
1019
|
+
|
|
1020
|
+
# Auto-link cross-references from dbxrefs (GEO, PMID, etc.)
|
|
1021
|
+
dbxrefs = exp_data.get("dbxrefs", []) or []
|
|
1022
|
+
auto_linked = []
|
|
1023
|
+
for xref in dbxrefs:
|
|
1024
|
+
if not isinstance(xref, str):
|
|
1025
|
+
continue
|
|
1026
|
+
if xref.startswith("GEO:"):
|
|
1027
|
+
ref_result = tracker.link_reference(
|
|
1028
|
+
accession,
|
|
1029
|
+
"geo_accession",
|
|
1030
|
+
xref.replace("GEO:", ""),
|
|
1031
|
+
"Auto-extracted from ENCODE dbxrefs",
|
|
1032
|
+
)
|
|
1033
|
+
if ref_result.get("action") == "linked":
|
|
1034
|
+
auto_linked.append({"type": "geo_accession", "id": xref.replace("GEO:", "")})
|
|
1035
|
+
elif xref.startswith("PMID:"):
|
|
1036
|
+
ref_result = tracker.link_reference(
|
|
1037
|
+
accession,
|
|
1038
|
+
"pmid",
|
|
1039
|
+
xref.replace("PMID:", ""),
|
|
1040
|
+
"Auto-extracted from ENCODE dbxrefs",
|
|
1041
|
+
)
|
|
1042
|
+
if ref_result.get("action") == "linked":
|
|
1043
|
+
auto_linked.append({"type": "pmid", "id": xref.replace("PMID:", "")})
|
|
1044
|
+
if auto_linked:
|
|
1045
|
+
output["auto_linked_references"] = auto_linked
|
|
1046
|
+
|
|
1047
|
+
# Fetch publications
|
|
1048
|
+
if fetch_publications:
|
|
1049
|
+
refs = exp_data.get("references", [])
|
|
1050
|
+
# If references are paths, fetch them (validate to prevent SSRF)
|
|
1051
|
+
resolved_refs = []
|
|
1052
|
+
for ref in refs:
|
|
1053
|
+
if isinstance(ref, str):
|
|
1054
|
+
try:
|
|
1055
|
+
safe_path = validate_encode_path(ref)
|
|
1056
|
+
ref_data = await client.get_json(safe_path, {"format": "json"})
|
|
1057
|
+
resolved_refs.append(ref_data)
|
|
1058
|
+
except Exception as e:
|
|
1059
|
+
logger.warning("Skipping ref %r: %s", ref, e)
|
|
1060
|
+
elif isinstance(ref, dict):
|
|
1061
|
+
resolved_refs.append(ref)
|
|
1062
|
+
|
|
1063
|
+
pubs = parse_encode_publications(resolved_refs)
|
|
1064
|
+
pub_count = tracker.store_publications(accession, pubs)
|
|
1065
|
+
output["publications_found"] = pub_count
|
|
1066
|
+
output["publications"] = pubs
|
|
1067
|
+
|
|
1068
|
+
# Fetch pipeline info
|
|
1069
|
+
if fetch_pipelines:
|
|
1070
|
+
analyses = exp_data.get("analyses", [])
|
|
1071
|
+
resolved_analyses = []
|
|
1072
|
+
for analysis in analyses:
|
|
1073
|
+
if isinstance(analysis, str):
|
|
1074
|
+
try:
|
|
1075
|
+
safe_path = validate_encode_path(analysis)
|
|
1076
|
+
a_data = await client.get_json(safe_path, {"format": "json"})
|
|
1077
|
+
resolved_analyses.append(a_data)
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
logger.warning("Skipping ref %r: %s", analysis, e)
|
|
1080
|
+
elif isinstance(analysis, dict):
|
|
1081
|
+
resolved_analyses.append(analysis)
|
|
1082
|
+
|
|
1083
|
+
pipelines = parse_encode_pipelines(resolved_analyses)
|
|
1084
|
+
pipe_count = tracker.store_pipeline_info(accession, pipelines)
|
|
1085
|
+
output["pipelines_found"] = pipe_count
|
|
1086
|
+
output["pipelines"] = pipelines
|
|
1087
|
+
|
|
1088
|
+
return json.dumps(output, indent=2)
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
# ======================================================================
|
|
1092
|
+
# Tool 12: List Tracked Experiments
|
|
1093
|
+
# ======================================================================
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="List Tracked Experiments")
|
|
1097
|
+
async def encode_list_tracked(
|
|
1098
|
+
assay_title: str | None = None,
|
|
1099
|
+
organism: str | None = None,
|
|
1100
|
+
organ: str | None = None,
|
|
1101
|
+
) -> str:
|
|
1102
|
+
"""List all experiments you've tracked locally, with optional filters.
|
|
1103
|
+
|
|
1104
|
+
Shows your local library of tracked ENCODE experiments, their metadata,
|
|
1105
|
+
publication counts, and derived file counts.
|
|
1106
|
+
|
|
1107
|
+
WHEN TO USE: Use to see all experiments in your local library. Filter by assay,
|
|
1108
|
+
organism, or organ.
|
|
1109
|
+
RELATED TOOLS: encode_summarize_collection, encode_export_data
|
|
1110
|
+
|
|
1111
|
+
Args:
|
|
1112
|
+
assay_title: Filter by assay type (partial match)
|
|
1113
|
+
organism: Filter by organism (partial match)
|
|
1114
|
+
organ: Filter by organ (partial match)
|
|
1115
|
+
|
|
1116
|
+
Returns:
|
|
1117
|
+
JSON with tracked experiments metadata table and tracker stats.
|
|
1118
|
+
"""
|
|
1119
|
+
tracker = _get_tracker()
|
|
1120
|
+
experiments = tracker.list_tracked_experiments(
|
|
1121
|
+
assay_title=assay_title,
|
|
1122
|
+
organism=organism,
|
|
1123
|
+
organ=organ,
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
# Build metadata table
|
|
1127
|
+
table = tracker.get_metadata_table([e["accession"] for e in experiments] if experiments else None)
|
|
1128
|
+
|
|
1129
|
+
# Remove raw_metadata from output
|
|
1130
|
+
for row in table:
|
|
1131
|
+
row.pop("raw_metadata", None)
|
|
1132
|
+
|
|
1133
|
+
return json.dumps(
|
|
1134
|
+
{
|
|
1135
|
+
"experiments": table,
|
|
1136
|
+
"count": len(table),
|
|
1137
|
+
"stats": tracker.stats,
|
|
1138
|
+
},
|
|
1139
|
+
indent=2,
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
# ======================================================================
|
|
1144
|
+
# Tool 13: Get Experiment Publications & Citations
|
|
1145
|
+
# ======================================================================
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="Get Citations")
|
|
1149
|
+
async def encode_get_citations(
|
|
1150
|
+
accession: str | None = None,
|
|
1151
|
+
export_format: Literal["json", "bibtex", "ris"] = "json",
|
|
1152
|
+
) -> str:
|
|
1153
|
+
"""Get publications and citations for tracked experiments.
|
|
1154
|
+
|
|
1155
|
+
Returns publication data with authors, journal, DOI, PMID.
|
|
1156
|
+
Can export as BibTeX or RIS (Endnote/Zotero/Mendeley compatible).
|
|
1157
|
+
|
|
1158
|
+
WHEN TO USE: Use to get publication data for tracked experiments. Supports
|
|
1159
|
+
BibTeX and RIS export for reference managers.
|
|
1160
|
+
RELATED TOOLS: encode_track_experiment, encode_link_reference
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
accession: Specific experiment accession. If None, returns all publications.
|
|
1164
|
+
export_format: Output format:
|
|
1165
|
+
- "json": Structured data (default)
|
|
1166
|
+
- "bibtex": BibTeX format for LaTeX
|
|
1167
|
+
- "ris": RIS format (Endnote, Zotero, Mendeley)
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
Publications in the requested format.
|
|
1171
|
+
"""
|
|
1172
|
+
validate_export_format(export_format)
|
|
1173
|
+
if accession:
|
|
1174
|
+
validate_accession(accession)
|
|
1175
|
+
tracker = _get_tracker()
|
|
1176
|
+
|
|
1177
|
+
if export_format == "bibtex":
|
|
1178
|
+
bibtex = tracker.export_citations_bibtex([accession] if accession else None)
|
|
1179
|
+
return bibtex if bibtex else "No publications found."
|
|
1180
|
+
|
|
1181
|
+
if export_format == "ris":
|
|
1182
|
+
ris = tracker.export_citations_ris([accession] if accession else None)
|
|
1183
|
+
return ris if ris else "No publications found."
|
|
1184
|
+
|
|
1185
|
+
# JSON format
|
|
1186
|
+
if accession:
|
|
1187
|
+
pubs = tracker.get_publications(accession)
|
|
1188
|
+
else:
|
|
1189
|
+
# Get all from all tracked experiments
|
|
1190
|
+
experiments = tracker.list_tracked_experiments()
|
|
1191
|
+
pubs = []
|
|
1192
|
+
for exp in experiments:
|
|
1193
|
+
exp_pubs = tracker.get_publications(exp["accession"])
|
|
1194
|
+
pubs.extend(exp_pubs)
|
|
1195
|
+
|
|
1196
|
+
return json.dumps(
|
|
1197
|
+
{
|
|
1198
|
+
"publications": pubs,
|
|
1199
|
+
"count": len(pubs),
|
|
1200
|
+
},
|
|
1201
|
+
indent=2,
|
|
1202
|
+
default=str,
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
# ======================================================================
|
|
1207
|
+
# Tool 14: Compare Experiments (Compatibility Analysis)
|
|
1208
|
+
# ======================================================================
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="Compare Experiments")
|
|
1212
|
+
async def encode_compare_experiments(
|
|
1213
|
+
accession1: str,
|
|
1214
|
+
accession2: str,
|
|
1215
|
+
) -> str:
|
|
1216
|
+
"""Analyze whether two ENCODE experiments are compatible for combined analysis.
|
|
1217
|
+
|
|
1218
|
+
Compares organism, genome assembly, assay type, biosample, organ, target,
|
|
1219
|
+
replication strategy, and lab to identify potential issues.
|
|
1220
|
+
|
|
1221
|
+
Both experiments must be tracked first (use encode_track_experiment).
|
|
1222
|
+
|
|
1223
|
+
WHEN TO USE: Use to check if two experiments are compatible for combined analysis.
|
|
1224
|
+
Both must be tracked first.
|
|
1225
|
+
RELATED TOOLS: encode_track_experiment, encode_list_tracked
|
|
1226
|
+
|
|
1227
|
+
Args:
|
|
1228
|
+
accession1: First experiment accession (e.g., "ENCSR133RZO")
|
|
1229
|
+
accession2: Second experiment accession (e.g., "ENCSR000AKS")
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
JSON compatibility report with verdict, issues, warnings, and recommendations.
|
|
1233
|
+
"""
|
|
1234
|
+
validate_accession(accession1)
|
|
1235
|
+
validate_accession(accession2)
|
|
1236
|
+
tracker = _get_tracker()
|
|
1237
|
+
result = tracker.analyze_compatibility(accession1, accession2)
|
|
1238
|
+
return json.dumps(result, indent=2)
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
# ======================================================================
|
|
1242
|
+
# Tool 15: Log Derived File (Provenance)
|
|
1243
|
+
# ======================================================================
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
@mcp.tool(annotations=_WRITE_LOCAL, title="Log Derived File")
|
|
1247
|
+
async def encode_log_derived_file(
|
|
1248
|
+
file_path: str,
|
|
1249
|
+
source_accessions: list[str],
|
|
1250
|
+
description: str = "",
|
|
1251
|
+
file_type: str = "",
|
|
1252
|
+
tool_used: str = "",
|
|
1253
|
+
parameters: str = "",
|
|
1254
|
+
) -> str:
|
|
1255
|
+
"""Log a file you've derived from ENCODE data for provenance tracking.
|
|
1256
|
+
|
|
1257
|
+
Use this when you create new files from ENCODE data (e.g., running a pipeline,
|
|
1258
|
+
filtering peaks, merging samples). This creates a provenance record linking
|
|
1259
|
+
your derived file back to the original ENCODE source data.
|
|
1260
|
+
|
|
1261
|
+
WHEN TO USE: Use after creating files from ENCODE data (filtered peaks, merged
|
|
1262
|
+
signals). Creates provenance chain back to source.
|
|
1263
|
+
RELATED TOOLS: encode_get_provenance, encode_download_files
|
|
1264
|
+
|
|
1265
|
+
Args:
|
|
1266
|
+
file_path: Path to the derived file you created
|
|
1267
|
+
source_accessions: List of ENCODE accessions this file was derived from
|
|
1268
|
+
(experiment or file accessions, e.g., ["ENCSR133RZO", "ENCFF635JIA"])
|
|
1269
|
+
description: What this derived file contains
|
|
1270
|
+
file_type: Type of file (e.g., "filtered_peaks", "merged_signal", "differential")
|
|
1271
|
+
tool_used: Tool/software used to create it (e.g., "bedtools intersect", "DESeq2")
|
|
1272
|
+
parameters: Parameters or command used
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
JSON with the provenance record ID.
|
|
1276
|
+
"""
|
|
1277
|
+
# Validate each source accession is a valid ENCODE identifier
|
|
1278
|
+
for acc in source_accessions:
|
|
1279
|
+
validate_accession(acc)
|
|
1280
|
+
|
|
1281
|
+
tracker = _get_tracker()
|
|
1282
|
+
row_id = tracker.log_derived_file(
|
|
1283
|
+
file_path=file_path,
|
|
1284
|
+
source_accessions=source_accessions,
|
|
1285
|
+
description=description,
|
|
1286
|
+
file_type=file_type,
|
|
1287
|
+
tool_used=tool_used,
|
|
1288
|
+
parameters=parameters,
|
|
1289
|
+
)
|
|
1290
|
+
return json.dumps(
|
|
1291
|
+
{
|
|
1292
|
+
"success": True,
|
|
1293
|
+
"record_id": row_id,
|
|
1294
|
+
"file_path": file_path,
|
|
1295
|
+
"source_accessions": source_accessions,
|
|
1296
|
+
"message": "Provenance logged. Use encode_get_provenance to view the full chain.",
|
|
1297
|
+
},
|
|
1298
|
+
indent=2,
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
# ======================================================================
|
|
1303
|
+
# Tool 16: Get Provenance
|
|
1304
|
+
# ======================================================================
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="Get File Provenance")
|
|
1308
|
+
async def encode_get_provenance(
|
|
1309
|
+
file_path: str | None = None,
|
|
1310
|
+
source_accession: str | None = None,
|
|
1311
|
+
) -> str:
|
|
1312
|
+
"""Get provenance information for derived files.
|
|
1313
|
+
|
|
1314
|
+
Shows the chain from your derived files back to original ENCODE data,
|
|
1315
|
+
including what tools and parameters were used.
|
|
1316
|
+
|
|
1317
|
+
WHEN TO USE: Use to trace a derived file back to original ENCODE data.
|
|
1318
|
+
Shows tools and parameters used.
|
|
1319
|
+
RELATED TOOLS: encode_log_derived_file
|
|
1320
|
+
|
|
1321
|
+
Args:
|
|
1322
|
+
file_path: Get provenance for a specific derived file
|
|
1323
|
+
source_accession: List all files derived from a specific ENCODE accession
|
|
1324
|
+
|
|
1325
|
+
Returns:
|
|
1326
|
+
JSON provenance chain or list of derived files.
|
|
1327
|
+
"""
|
|
1328
|
+
tracker = _get_tracker()
|
|
1329
|
+
|
|
1330
|
+
if file_path:
|
|
1331
|
+
chain = tracker.get_provenance_chain(file_path)
|
|
1332
|
+
return json.dumps(chain, indent=2, default=str)
|
|
1333
|
+
|
|
1334
|
+
derived = tracker.get_derived_files(source_accession)
|
|
1335
|
+
return json.dumps(
|
|
1336
|
+
{
|
|
1337
|
+
"derived_files": derived,
|
|
1338
|
+
"count": len(derived),
|
|
1339
|
+
},
|
|
1340
|
+
indent=2,
|
|
1341
|
+
default=str,
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
# ======================================================================
|
|
1346
|
+
# Tool 17: Export Tracked Data
|
|
1347
|
+
# ======================================================================
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="Export Tracked Data")
|
|
1351
|
+
async def encode_export_data(
|
|
1352
|
+
format: Literal["csv", "tsv", "json"] = "csv",
|
|
1353
|
+
assay_title: str | None = None,
|
|
1354
|
+
organism: str | None = None,
|
|
1355
|
+
organ: str | None = None,
|
|
1356
|
+
) -> str:
|
|
1357
|
+
"""Export tracked experiments as a table (CSV, TSV, or JSON).
|
|
1358
|
+
|
|
1359
|
+
Creates a tabular export of all tracked experiments with metadata,
|
|
1360
|
+
publication counts, PMIDs, and derived file counts. Useful for loading
|
|
1361
|
+
into Excel, R, pandas, or sharing with collaborators.
|
|
1362
|
+
|
|
1363
|
+
PMIDs in the output can be directly used with PubMed MCP tools for
|
|
1364
|
+
further literature analysis.
|
|
1365
|
+
|
|
1366
|
+
WHEN TO USE: Use to create shareable tables of tracked experiments (CSV, TSV, JSON).
|
|
1367
|
+
Good for manuscripts and reports.
|
|
1368
|
+
RELATED TOOLS: encode_list_tracked, encode_summarize_collection
|
|
1369
|
+
|
|
1370
|
+
Args:
|
|
1371
|
+
format: Output format:
|
|
1372
|
+
- "csv": Comma-separated values (default, for Excel/spreadsheets)
|
|
1373
|
+
- "tsv": Tab-separated values (for R, pandas)
|
|
1374
|
+
- "json": JSON array (for programmatic use)
|
|
1375
|
+
assay_title: Filter by assay type (partial match)
|
|
1376
|
+
organism: Filter by organism (partial match)
|
|
1377
|
+
organ: Filter by organ (partial match)
|
|
1378
|
+
|
|
1379
|
+
Returns:
|
|
1380
|
+
Formatted table data in the requested format.
|
|
1381
|
+
"""
|
|
1382
|
+
validate_data_export_format(format)
|
|
1383
|
+
tracker = _get_tracker()
|
|
1384
|
+
result = tracker.export_tracked_data(
|
|
1385
|
+
format=format,
|
|
1386
|
+
assay_title=assay_title,
|
|
1387
|
+
organism=organism,
|
|
1388
|
+
organ=organ,
|
|
1389
|
+
)
|
|
1390
|
+
if not result:
|
|
1391
|
+
return json.dumps({"message": "No tracked experiments found matching filters."})
|
|
1392
|
+
return result
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
# ======================================================================
|
|
1396
|
+
# Tool 18: Summarize Collection
|
|
1397
|
+
# ======================================================================
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="Summarize Collection")
|
|
1401
|
+
async def encode_summarize_collection(
|
|
1402
|
+
assay_title: str | None = None,
|
|
1403
|
+
organism: str | None = None,
|
|
1404
|
+
organ: str | None = None,
|
|
1405
|
+
) -> str:
|
|
1406
|
+
"""Summarize your tracked experiment collection with grouped statistics.
|
|
1407
|
+
|
|
1408
|
+
Provides an overview of tracked experiments grouped by assay type, target,
|
|
1409
|
+
organism, organ, biosample type, and lab. Shows total counts for publications,
|
|
1410
|
+
derived files, and external references.
|
|
1411
|
+
|
|
1412
|
+
Useful when tracking 10+ experiments and needing a bird's-eye view of your
|
|
1413
|
+
research data collection.
|
|
1414
|
+
|
|
1415
|
+
WHEN TO USE: Use for a bird's-eye view of tracked experiments grouped by assay,
|
|
1416
|
+
target, organ. Best for 10+ tracked experiments.
|
|
1417
|
+
RELATED TOOLS: encode_list_tracked, encode_export_data
|
|
1418
|
+
|
|
1419
|
+
Args:
|
|
1420
|
+
assay_title: Filter by assay type (partial match)
|
|
1421
|
+
organism: Filter by organism (partial match)
|
|
1422
|
+
organ: Filter by organ (partial match)
|
|
1423
|
+
|
|
1424
|
+
Returns:
|
|
1425
|
+
JSON summary with experiment counts grouped by multiple dimensions.
|
|
1426
|
+
"""
|
|
1427
|
+
tracker = _get_tracker()
|
|
1428
|
+
summary = tracker.summarize_collection(
|
|
1429
|
+
assay_title=assay_title,
|
|
1430
|
+
organism=organism,
|
|
1431
|
+
organ=organ,
|
|
1432
|
+
)
|
|
1433
|
+
return json.dumps(summary, indent=2)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
# ======================================================================
|
|
1437
|
+
# Tool 19: Link External Reference
|
|
1438
|
+
# ======================================================================
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
@mcp.tool(annotations=_WRITE_LOCAL, title="Link External Reference")
|
|
1442
|
+
async def encode_link_reference(
|
|
1443
|
+
experiment_accession: str,
|
|
1444
|
+
reference_type: Literal["pmid", "doi", "nct_id", "preprint_doi", "geo_accession", "other"],
|
|
1445
|
+
reference_id: str,
|
|
1446
|
+
description: str = "",
|
|
1447
|
+
) -> str:
|
|
1448
|
+
"""Link an external reference to a tracked ENCODE experiment.
|
|
1449
|
+
|
|
1450
|
+
This is the cross-server bridge. Attach PubMed IDs, bioRxiv DOIs,
|
|
1451
|
+
ClinicalTrials.gov NCT IDs, GEO accessions, or any external identifier
|
|
1452
|
+
to your tracked experiments for provenance and cross-referencing.
|
|
1453
|
+
|
|
1454
|
+
After finding a relevant paper with PubMed MCP or a preprint on bioRxiv,
|
|
1455
|
+
link it to the ENCODE experiment for future reference.
|
|
1456
|
+
|
|
1457
|
+
WHEN TO USE: Use to attach external IDs (PMID, DOI, GEO, NCT) to tracked
|
|
1458
|
+
experiments for cross-referencing.
|
|
1459
|
+
RELATED TOOLS: encode_get_references, encode_get_citations
|
|
1460
|
+
|
|
1461
|
+
Args:
|
|
1462
|
+
experiment_accession: ENCODE experiment accession (e.g., "ENCSR133RZO")
|
|
1463
|
+
reference_type: Type of external reference:
|
|
1464
|
+
- "pmid": PubMed ID (e.g., "32728249")
|
|
1465
|
+
- "doi": DOI (e.g., "10.1038/s41586-020-2493-4")
|
|
1466
|
+
- "nct_id": ClinicalTrials.gov ID (e.g., "NCT04567890")
|
|
1467
|
+
- "preprint_doi": bioRxiv/medRxiv DOI
|
|
1468
|
+
- "geo_accession": GEO accession (e.g., "GSE123456")
|
|
1469
|
+
- "other": Any other identifier
|
|
1470
|
+
reference_id: The actual identifier value
|
|
1471
|
+
description: Optional description of why this reference is linked
|
|
1472
|
+
|
|
1473
|
+
Returns:
|
|
1474
|
+
JSON with linking result.
|
|
1475
|
+
"""
|
|
1476
|
+
validate_accession(experiment_accession)
|
|
1477
|
+
validate_reference_type(reference_type)
|
|
1478
|
+
tracker = _get_tracker()
|
|
1479
|
+
result = tracker.link_reference(
|
|
1480
|
+
accession=experiment_accession,
|
|
1481
|
+
reference_type=reference_type,
|
|
1482
|
+
reference_id=reference_id,
|
|
1483
|
+
description=description,
|
|
1484
|
+
)
|
|
1485
|
+
return json.dumps(result, indent=2)
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
# ======================================================================
|
|
1489
|
+
# Tool 20: Get External References
|
|
1490
|
+
# ======================================================================
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
@mcp.tool(annotations=_READONLY_LOCAL, title="Get Linked References")
|
|
1494
|
+
async def encode_get_references(
|
|
1495
|
+
experiment_accession: str | None = None,
|
|
1496
|
+
reference_type: Literal["pmid", "doi", "nct_id", "preprint_doi", "geo_accession", "other"] | None = None,
|
|
1497
|
+
) -> str:
|
|
1498
|
+
"""Get external references linked to tracked experiments.
|
|
1499
|
+
|
|
1500
|
+
Returns PMIDs, DOIs, NCT IDs, GEO accessions and other identifiers
|
|
1501
|
+
linked to experiments. These identifiers can be directly passed to
|
|
1502
|
+
PubMed, bioRxiv, ClinicalTrials.gov, or other MCP tools.
|
|
1503
|
+
|
|
1504
|
+
WHEN TO USE: Use to retrieve external references linked to experiments.
|
|
1505
|
+
PMIDs can be passed to PubMed MCP tools.
|
|
1506
|
+
RELATED TOOLS: encode_link_reference, encode_get_citations
|
|
1507
|
+
|
|
1508
|
+
Args:
|
|
1509
|
+
experiment_accession: Filter by specific experiment (optional)
|
|
1510
|
+
reference_type: Filter by reference type (optional):
|
|
1511
|
+
"pmid", "doi", "nct_id", "preprint_doi", "geo_accession", "other"
|
|
1512
|
+
|
|
1513
|
+
Returns:
|
|
1514
|
+
JSON with linked external references.
|
|
1515
|
+
"""
|
|
1516
|
+
if experiment_accession:
|
|
1517
|
+
validate_accession(experiment_accession)
|
|
1518
|
+
tracker = _get_tracker()
|
|
1519
|
+
refs = tracker.get_references(
|
|
1520
|
+
accession=experiment_accession,
|
|
1521
|
+
reference_type=reference_type,
|
|
1522
|
+
)
|
|
1523
|
+
return json.dumps(
|
|
1524
|
+
{
|
|
1525
|
+
"references": refs,
|
|
1526
|
+
"count": len(refs),
|
|
1527
|
+
},
|
|
1528
|
+
indent=2,
|
|
1529
|
+
default=str,
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
# ======================================================================
|
|
1534
|
+
# Entry point
|
|
1535
|
+
# ======================================================================
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def main():
|
|
1539
|
+
"""Run the MCP server."""
|
|
1540
|
+
mcp.run()
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
if __name__ == "__main__":
|
|
1544
|
+
main()
|