modulector-sdk 2.3.0__tar.gz → 2.5.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modulector-sdk
3
- Version: 2.3.0
3
+ Version: 2.5.2
4
4
  Summary: Typed Python SDK for the Modulector API.
5
5
  Author: omicsdatascience
6
6
  License-Expression: MIT
@@ -25,6 +25,7 @@ Classifier: Typing :: Typed
25
25
  Requires-Python: >=3.10
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
+ Requires-Dist: mcp==1.27.2
28
29
  Requires-Dist: requests==2.31.0
29
30
  Dynamic: license-file
30
31
 
@@ -63,6 +64,46 @@ MODULECTOR_API_BASE_URL=https://your-modulector.example.org python script.py
63
64
  Every service function also accepts a `base_url` keyword argument for
64
65
  per-request overrides.
65
66
 
67
+ ## MCP server
68
+
69
+ The SDK also installs a Model Context Protocol server that exposes the same
70
+ service functions as LLM-callable tools.
71
+
72
+ For local stdio MCP clients:
73
+
74
+ ```bash
75
+ modulector-mcp
76
+ ```
77
+
78
+ Example client configuration:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "modulector": {
84
+ "command": "modulector-mcp"
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ For Streamable HTTP clients:
91
+
92
+ ```bash
93
+ modulector-mcp --transport streamable-http --host 127.0.0.1 --port 8000
94
+ ```
95
+
96
+ Then connect the client to `http://127.0.0.1:8000/mcp`.
97
+
98
+ The server includes tools for miRNA target interactions, miRNA details and
99
+ aliases, miRNA identifier resolution, methylation site lookup and annotation,
100
+ disease associations, drug associations, and PubMed news subscriptions. Use
101
+ `MODULECTOR_API_BASE_URL` or each tool's `base_url` argument to target a custom
102
+ Modulector deployment.
103
+
104
+ When deployed with the root Docker Compose stack, nginx proxies the MCP
105
+ Streamable HTTP endpoint at `https://<mydomain.com>/mcp`.
106
+
66
107
  ## Development
67
108
 
68
109
  Build this SDK from the `sdk/` directory:
@@ -0,0 +1,84 @@
1
+ # Modulector SDK
2
+
3
+ Typed Python SDK for the Modulector API. The package contains client helpers for
4
+ querying miRNA target interactions, miRNA aliases, methylation sites, disease
5
+ associations, drug associations, and PubMed subscriptions without installing the
6
+ Django backend dependencies.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install modulector-sdk
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from modulector_sdk import get_mirna_details, get_mirna_target_interactions
18
+
19
+ details = get_mirna_details("hsa-miR-21-5p")
20
+ interactions = get_mirna_target_interactions(
21
+ mirna="hsa-miR-21-5p",
22
+ gene="PTEN",
23
+ include_pubmeds=True,
24
+ )
25
+ ```
26
+
27
+ Set `MODULECTOR_API_BASE_URL` to target a different Modulector deployment:
28
+
29
+ ```bash
30
+ MODULECTOR_API_BASE_URL=https://your-modulector.example.org python script.py
31
+ ```
32
+
33
+ Every service function also accepts a `base_url` keyword argument for
34
+ per-request overrides.
35
+
36
+ ## MCP server
37
+
38
+ The SDK also installs a Model Context Protocol server that exposes the same
39
+ service functions as LLM-callable tools.
40
+
41
+ For local stdio MCP clients:
42
+
43
+ ```bash
44
+ modulector-mcp
45
+ ```
46
+
47
+ Example client configuration:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "modulector": {
53
+ "command": "modulector-mcp"
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ For Streamable HTTP clients:
60
+
61
+ ```bash
62
+ modulector-mcp --transport streamable-http --host 127.0.0.1 --port 8000
63
+ ```
64
+
65
+ Then connect the client to `http://127.0.0.1:8000/mcp`.
66
+
67
+ The server includes tools for miRNA target interactions, miRNA details and
68
+ aliases, miRNA identifier resolution, methylation site lookup and annotation,
69
+ disease associations, drug associations, and PubMed news subscriptions. Use
70
+ `MODULECTOR_API_BASE_URL` or each tool's `base_url` argument to target a custom
71
+ Modulector deployment.
72
+
73
+ When deployed with the root Docker Compose stack, nginx proxies the MCP
74
+ Streamable HTTP endpoint at `https://<mydomain.com>/mcp`.
75
+
76
+ ## Development
77
+
78
+ Build this SDK from the `sdk/` directory:
79
+
80
+ ```bash
81
+ uv build
82
+ ```
83
+
84
+ The generated source distribution and wheel are written to `sdk/dist/`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modulector-sdk"
7
- version = "2.3.0"
7
+ version = "2.5.2"
8
8
  description = "Typed Python SDK for the Modulector API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -36,6 +36,7 @@ classifiers = [
36
36
  "Typing :: Typed",
37
37
  ]
38
38
  dependencies = [
39
+ "mcp==1.27.2",
39
40
  "requests==2.31.0",
40
41
  ]
41
42
 
@@ -45,6 +46,9 @@ Homepage = "https://modulector.multiomix.org/"
45
46
  Issues = "https://github.com/omics-datascience/modulector/issues"
46
47
  Repository = "https://github.com/omics-datascience/modulector"
47
48
 
49
+ [project.scripts]
50
+ modulector-mcp = "modulector_sdk.mcp_server:main"
51
+
48
52
  [tool.setuptools.packages.find]
49
53
  where = ["src"]
50
54
 
@@ -0,0 +1,538 @@
1
+ """MCP server exposing Modulector SDK services to LLM clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from typing import Any, Final, Literal, TypeVar, cast
8
+ from urllib.parse import urlparse
9
+
10
+ from mcp.server.fastmcp import FastMCP
11
+ from mcp.server.transport_security import TransportSecuritySettings
12
+
13
+ from . import services
14
+ from .utils import PaginatedResponse
15
+
16
+ T = TypeVar("T")
17
+ Transport = Literal["stdio", "sse", "streamable-http"]
18
+
19
+ SERVER_INSTRUCTIONS: Final[str] = (
20
+ "Use Modulector to help researchers inspect human miRNA target "
21
+ "interactions, miRNA aliases, disease and drug associations, and "
22
+ "Illumina methylation site annotations. Prefer finder tools when the "
23
+ "user gives partial or uncertain identifiers, then use the detail or "
24
+ "paginated tools with explicit identifiers. The returned data comes from "
25
+ "the configured Modulector API deployment."
26
+ )
27
+ PUBLIC_BASE_URL: Final[str] = os.getenv(
28
+ "MODULECTOR_PUBLIC_BASE_URL",
29
+ services.MODULECTOR_API_BASE_URL,
30
+ )
31
+
32
+ mcp = FastMCP(
33
+ name="modulector",
34
+ instructions=SERVER_INSTRUCTIONS,
35
+ website_url=PUBLIC_BASE_URL,
36
+ )
37
+
38
+
39
+ @mcp.resource(
40
+ "modulector://about",
41
+ title="About Modulector",
42
+ description="Overview of the Modulector data exposed by this MCP server.",
43
+ mime_type="text/markdown",
44
+ )
45
+ def about_modulector() -> str:
46
+ """Return a concise overview of this MCP server.
47
+
48
+ :return: Markdown overview for LLM context.
49
+ """
50
+
51
+ return (
52
+ "# Modulector\n\n"
53
+ "Modulector provides programmatic access to curated human miRNA target "
54
+ "interactions, miRNA aliases, miRNA-disease associations, "
55
+ "miRNA-drug associations, and Illumina Infinium MethylationEPIC 2.0 "
56
+ "site annotations.\n\n"
57
+ f"Default API base URL: `{services.MODULECTOR_API_BASE_URL}`\n\n"
58
+ f"Public website URL: `{PUBLIC_BASE_URL}`\n\n"
59
+ "Use the finder tools for partial identifiers, then query detail or "
60
+ "paginated tools with exact miRNA, gene, disease, drug, or methylation "
61
+ "site filters."
62
+ )
63
+
64
+
65
+ @mcp.prompt(
66
+ name="modulector_research_plan",
67
+ title="Modulector Research Plan",
68
+ description="Guide an LLM through a focused Modulector research lookup.",
69
+ )
70
+ def modulector_research_plan(research_question: str) -> str:
71
+ """Return a prompt for planning a Modulector lookup.
72
+
73
+ :param research_question: Research question to investigate.
74
+ :return: Prompt text that guides tool selection and result synthesis.
75
+ """
76
+
77
+ return (
78
+ "Plan a Modulector lookup for this research question:\n\n"
79
+ f"{research_question}\n\n"
80
+ "First identify whether the task involves miRNA identifiers, gene "
81
+ "symbols, diseases, drugs, or methylation sites. Use finder or alias "
82
+ "tools to normalize uncertain identifiers. Then retrieve the smallest "
83
+ "useful page of records, include PubMed links only when needed, and "
84
+ "summarize the evidence with clear caveats about database scope."
85
+ )
86
+
87
+
88
+ @mcp.tool()
89
+ def get_mirna_target_interactions(
90
+ mirna: str | None = None,
91
+ gene: str | None = None,
92
+ score: float | None = None,
93
+ include_pubmeds: bool | None = None,
94
+ ordering: str | None = None,
95
+ search: str | None = None,
96
+ page: int | None = None,
97
+ page_size: int | None = None,
98
+ base_url: str | None = None,
99
+ timeout: float = 30.0,
100
+ ) -> dict[str, Any]:
101
+ """Return miRNA-gene target interactions from Modulector.
102
+
103
+ :param mirna: miRNA accession ID or miRBase mature name.
104
+ :param gene: Gene symbol.
105
+ :param score: Minimum MirDIP score between 0 and 1.
106
+ :param include_pubmeds: Whether to include PubMed URLs for interactions.
107
+ :param ordering: Comma-separated ordering fields, such as ``-score,gene``.
108
+ :param search: Search term for supported server-side fields.
109
+ :param page: Page number to request.
110
+ :param page_size: Number of records to request.
111
+ :param base_url: Optional Modulector API deployment URL.
112
+ :param timeout: Request timeout in seconds.
113
+ :return: Paginated interaction records.
114
+ """
115
+
116
+ return _page_to_dict(
117
+ services.get_mirna_target_interactions(
118
+ base_url=_base_url(base_url),
119
+ mirna=mirna,
120
+ gene=gene,
121
+ score=score,
122
+ include_pubmeds=include_pubmeds,
123
+ ordering=ordering,
124
+ search=search,
125
+ page=page,
126
+ page_size=page_size,
127
+ timeout=timeout,
128
+ )
129
+ )
130
+
131
+
132
+ @mcp.tool()
133
+ def get_mirna_details(
134
+ mirna: str,
135
+ base_url: str | None = None,
136
+ timeout: float = 30.0,
137
+ ) -> dict[str, Any]:
138
+ """Return sequence, aliases, accession, and source links for one miRNA.
139
+
140
+ :param mirna: miRNA identifier, such as a mature miRNA ID or accession ID.
141
+ :param base_url: Optional Modulector API deployment URL.
142
+ :param timeout: Request timeout in seconds.
143
+ :return: miRNA detail record.
144
+ """
145
+
146
+ return cast(
147
+ dict[str, Any],
148
+ services.get_mirna_details(
149
+ mirna,
150
+ base_url=_base_url(base_url),
151
+ timeout=timeout,
152
+ ),
153
+ )
154
+
155
+
156
+ @mcp.tool()
157
+ def get_mirna_aliases(
158
+ mature_mirna: str | None = None,
159
+ mirbase_accession_id: str | None = None,
160
+ previous_mature_mirna: str | None = None,
161
+ search: str | None = None,
162
+ ordering: str | None = None,
163
+ page: int | None = None,
164
+ page_size: int | None = None,
165
+ base_url: str | None = None,
166
+ timeout: float = 30.0,
167
+ ) -> dict[str, Any]:
168
+ """Return miRBase accession and mature miRNA alias records.
169
+
170
+ :param mature_mirna: Exact mature miRNA filter.
171
+ :param mirbase_accession_id: Exact miRBase accession ID filter.
172
+ :param previous_mature_mirna: Exact previous mature miRNA filter.
173
+ :param search: Search across mature IDs, previous IDs, and accessions.
174
+ :param ordering: Comma-separated ordering fields.
175
+ :param page: Page number to request.
176
+ :param page_size: Number of records to request.
177
+ :param base_url: Optional Modulector API deployment URL.
178
+ :param timeout: Request timeout in seconds.
179
+ :return: Paginated miRNA alias records.
180
+ """
181
+
182
+ return _page_to_dict(
183
+ services.get_mirna_aliases(
184
+ base_url=_base_url(base_url),
185
+ mature_mirna=mature_mirna,
186
+ mirbase_accession_id=mirbase_accession_id,
187
+ previous_mature_mirna=previous_mature_mirna,
188
+ search=search,
189
+ ordering=ordering,
190
+ page=page,
191
+ page_size=page_size,
192
+ timeout=timeout,
193
+ )
194
+ )
195
+
196
+
197
+ @mcp.tool()
198
+ def find_mirna_codes(
199
+ query: str,
200
+ limit: int | None = None,
201
+ base_url: str | None = None,
202
+ timeout: float = 30.0,
203
+ ) -> list[str]:
204
+ """Return miRNA identifiers matching a partial search string.
205
+
206
+ :param query: miRNA search string.
207
+ :param limit: Maximum number of identifiers to return.
208
+ :param base_url: Optional Modulector API deployment URL.
209
+ :param timeout: Request timeout in seconds.
210
+ :return: Matching miRNA IDs or accession IDs.
211
+ """
212
+
213
+ return services.find_mirna_codes(
214
+ query,
215
+ base_url=_base_url(base_url),
216
+ limit=limit,
217
+ timeout=timeout,
218
+ )
219
+
220
+
221
+ @mcp.tool()
222
+ def get_mirna_codes(
223
+ mirna_codes: list[str],
224
+ base_url: str | None = None,
225
+ timeout: float = 30.0,
226
+ ) -> dict[str, str | None]:
227
+ """Resolve miRNA identifiers to approved miRBase accessions.
228
+
229
+ :param mirna_codes: miRNA identifiers to resolve.
230
+ :param base_url: Optional Modulector API deployment URL.
231
+ :param timeout: Request timeout in seconds.
232
+ :return: Mapping from requested identifiers to accession IDs or null.
233
+ """
234
+
235
+ return services.get_mirna_codes(
236
+ mirna_codes,
237
+ base_url=_base_url(base_url),
238
+ timeout=timeout,
239
+ )
240
+
241
+
242
+ @mcp.tool()
243
+ def find_methylation_sites(
244
+ query: str,
245
+ limit: int | None = None,
246
+ base_url: str | None = None,
247
+ timeout: float = 30.0,
248
+ ) -> list[str]:
249
+ """Return methylation site names matching a partial search string.
250
+
251
+ :param query: Methylation site search string.
252
+ :param limit: Maximum number of site names to return.
253
+ :param base_url: Optional Modulector API deployment URL.
254
+ :param timeout: Request timeout in seconds.
255
+ :return: Matching methylation site names.
256
+ """
257
+
258
+ return services.find_methylation_sites(
259
+ query,
260
+ base_url=_base_url(base_url),
261
+ limit=limit,
262
+ timeout=timeout,
263
+ )
264
+
265
+
266
+ @mcp.tool()
267
+ def get_methylation_sites(
268
+ methylation_sites: list[str],
269
+ base_url: str | None = None,
270
+ timeout: float = 30.0,
271
+ ) -> dict[str, list[str]]:
272
+ """Resolve methylation identifiers to EPIC 2.0 site names.
273
+
274
+ :param methylation_sites: Illumina methylation site names or identifiers.
275
+ :param base_url: Optional Modulector API deployment URL.
276
+ :param timeout: Request timeout in seconds.
277
+ :return: Mapping from requested identifiers to current EPIC 2.0 names.
278
+ """
279
+
280
+ return services.get_methylation_sites(
281
+ methylation_sites,
282
+ base_url=_base_url(base_url),
283
+ timeout=timeout,
284
+ )
285
+
286
+
287
+ @mcp.tool()
288
+ def get_methylation_site_genes(
289
+ methylation_sites: list[str],
290
+ base_url: str | None = None,
291
+ timeout: float = 30.0,
292
+ ) -> dict[str, list[str]]:
293
+ """Return genes associated with methylation site identifiers.
294
+
295
+ :param methylation_sites: Illumina methylation site names or identifiers.
296
+ :param base_url: Optional Modulector API deployment URL.
297
+ :param timeout: Request timeout in seconds.
298
+ :return: Mapping from requested site identifiers to associated genes.
299
+ """
300
+
301
+ return services.get_methylation_site_genes(
302
+ methylation_sites,
303
+ base_url=_base_url(base_url),
304
+ timeout=timeout,
305
+ )
306
+
307
+
308
+ @mcp.tool()
309
+ def get_methylation_details(
310
+ methylation_site: str,
311
+ base_url: str | None = None,
312
+ timeout: float = 30.0,
313
+ ) -> dict[str, Any]:
314
+ """Return detailed annotations for one methylation site.
315
+
316
+ :param methylation_site: Methylation site name from EPIC 2.0.
317
+ :param base_url: Optional Modulector API deployment URL.
318
+ :param timeout: Request timeout in seconds.
319
+ :return: Methylation site detail record.
320
+ """
321
+
322
+ return cast(
323
+ dict[str, Any],
324
+ services.get_methylation_details(
325
+ methylation_site,
326
+ base_url=_base_url(base_url),
327
+ timeout=timeout,
328
+ ),
329
+ )
330
+
331
+
332
+ @mcp.tool()
333
+ def get_diseases(
334
+ mirna: str | None = None,
335
+ search: str | None = None,
336
+ ordering: str | None = None,
337
+ page: int | None = None,
338
+ page_size: int | None = None,
339
+ base_url: str | None = None,
340
+ timeout: float = 30.0,
341
+ ) -> dict[str, Any]:
342
+ """Return miRNA and human disease association records.
343
+
344
+ :param mirna: miRNA code or accession ID filter.
345
+ :param search: Case-insensitive disease search term.
346
+ :param ordering: Comma-separated ordering fields.
347
+ :param page: Page number to request.
348
+ :param page_size: Number of records to request.
349
+ :param base_url: Optional Modulector API deployment URL.
350
+ :param timeout: Request timeout in seconds.
351
+ :return: Paginated disease association records.
352
+ """
353
+
354
+ return _page_to_dict(
355
+ services.get_diseases(
356
+ base_url=_base_url(base_url),
357
+ mirna=mirna,
358
+ search=search,
359
+ ordering=ordering,
360
+ page=page,
361
+ page_size=page_size,
362
+ timeout=timeout,
363
+ )
364
+ )
365
+
366
+
367
+ @mcp.tool()
368
+ def get_drugs(
369
+ mirna: str | None = None,
370
+ fda_approved: bool | None = None,
371
+ search: str | None = None,
372
+ ordering: str | None = None,
373
+ page: int | None = None,
374
+ page_size: int | None = None,
375
+ base_url: str | None = None,
376
+ timeout: float = 30.0,
377
+ ) -> dict[str, Any]:
378
+ """Return drug or small molecule records affecting miRNA expression.
379
+
380
+ :param mirna: miRNA code or accession ID filter.
381
+ :param fda_approved: Optional FDA approval filter.
382
+ :param search: Search term for condition, molecule, or expression fields.
383
+ :param ordering: Comma-separated ordering fields.
384
+ :param page: Page number to request.
385
+ :param page_size: Number of records to request.
386
+ :param base_url: Optional Modulector API deployment URL.
387
+ :param timeout: Request timeout in seconds.
388
+ :return: Paginated drug association records.
389
+ """
390
+
391
+ return _page_to_dict(
392
+ services.get_drugs(
393
+ base_url=_base_url(base_url),
394
+ mirna=mirna,
395
+ fda_approved=fda_approved,
396
+ search=search,
397
+ ordering=ordering,
398
+ page=page,
399
+ page_size=page_size,
400
+ timeout=timeout,
401
+ )
402
+ )
403
+
404
+
405
+ def main() -> None:
406
+ """Run the Modulector MCP server.
407
+
408
+ :return: Nothing.
409
+ """
410
+
411
+ args = _parse_args()
412
+ transport = cast(Transport, args.transport)
413
+ if transport != "stdio":
414
+ mcp.settings.host = args.host
415
+ mcp.settings.port = args.port
416
+ mcp.settings.json_response = args.json_response
417
+ _configure_http_transport_security()
418
+ mcp.run(transport=transport)
419
+
420
+
421
+ def _parse_args() -> argparse.Namespace:
422
+ """Parse command-line arguments for the MCP server.
423
+
424
+ :return: Parsed command-line arguments.
425
+ """
426
+
427
+ parser = argparse.ArgumentParser(
428
+ description="Run the Modulector MCP server.",
429
+ )
430
+ parser.add_argument(
431
+ "--transport",
432
+ choices=("stdio", "sse", "streamable-http"),
433
+ default="stdio",
434
+ help="MCP transport to use. Defaults to stdio.",
435
+ )
436
+ parser.add_argument(
437
+ "--host",
438
+ default="127.0.0.1",
439
+ help="Host for HTTP transports. Defaults to 127.0.0.1.",
440
+ )
441
+ parser.add_argument(
442
+ "--port",
443
+ type=int,
444
+ default=8000,
445
+ help="Port for HTTP transports. Defaults to 8000.",
446
+ )
447
+ parser.add_argument(
448
+ "--json-response",
449
+ action="store_true",
450
+ help="Use JSON responses for Streamable HTTP instead of SSE streams.",
451
+ )
452
+ return parser.parse_args()
453
+
454
+
455
+ def _base_url(base_url: str | None) -> str:
456
+ """Return the requested or default Modulector API base URL.
457
+
458
+ :param base_url: Optional Modulector API deployment URL.
459
+ :return: API base URL.
460
+ """
461
+
462
+ return base_url or services.MODULECTOR_API_BASE_URL
463
+
464
+
465
+ def _configure_http_transport_security() -> None:
466
+ """Configure DNS rebinding protection for HTTP MCP transports.
467
+
468
+ :return: None.
469
+ """
470
+
471
+ mcp.settings.transport_security = TransportSecuritySettings(
472
+ allowed_hosts=_allowed_hosts(),
473
+ allowed_origins=_allowed_origins(),
474
+ )
475
+
476
+
477
+ def _allowed_hosts() -> list[str]:
478
+ """Return allowed Host headers for the MCP HTTP transport.
479
+
480
+ :return: Host header allow-list.
481
+ """
482
+
483
+ configured_hosts = _csv_env("MODULECTOR_MCP_ALLOWED_HOSTS")
484
+ if configured_hosts:
485
+ return configured_hosts
486
+
487
+ parsed_public_url = urlparse(PUBLIC_BASE_URL)
488
+ public_host = parsed_public_url.netloc
489
+ hosts = ["127.0.0.1:*", "localhost:*", "[::1]:*"]
490
+ if public_host:
491
+ hosts.append(public_host)
492
+ return hosts
493
+
494
+
495
+ def _allowed_origins() -> list[str]:
496
+ """Return allowed Origin headers for the MCP HTTP transport.
497
+
498
+ :return: Origin header allow-list.
499
+ """
500
+
501
+ configured_origins = _csv_env("MODULECTOR_MCP_ALLOWED_ORIGINS")
502
+ if configured_origins:
503
+ return configured_origins
504
+
505
+ parsed_public_url = urlparse(PUBLIC_BASE_URL)
506
+ origins = ["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"]
507
+ if parsed_public_url.scheme and parsed_public_url.netloc:
508
+ origins.append(f"{parsed_public_url.scheme}://{parsed_public_url.netloc}")
509
+ return origins
510
+
511
+
512
+ def _csv_env(name: str) -> list[str]:
513
+ """Return comma-separated environment variable values.
514
+
515
+ :param name: Environment variable name.
516
+ :return: Non-empty stripped values.
517
+ """
518
+
519
+ return [value.strip() for value in os.getenv(name, "").split(",") if value.strip()]
520
+
521
+
522
+ def _page_to_dict(page: PaginatedResponse[T]) -> dict[str, Any]:
523
+ """Convert a paginated SDK response into a JSON-serializable mapping.
524
+
525
+ :param page: SDK paginated response.
526
+ :return: Mapping with pagination metadata and results.
527
+ """
528
+
529
+ return {
530
+ "count": page.count,
531
+ "next": page.next,
532
+ "previous": page.previous,
533
+ "results": page.results,
534
+ }
535
+
536
+
537
+ if __name__ == "__main__":
538
+ main()
@@ -191,6 +191,15 @@ def _with_pagination_params(
191
191
  page: int | None,
192
192
  page_size: int | None,
193
193
  ) -> dict[str, Any] | None:
194
+ """
195
+ Return a new params dict with pagination parameters added, or None if no params.
196
+ :param params: Parameters to include in the request, or None for no parameters.
197
+ :param page: Page number to include in the request, or None for no page parameter.
198
+ :param page_size: Number of items per page to include in the request, or None for no page_size parameter.
199
+ :raises ValueError: If page is less than 1.
200
+ :raises ValueError: If page_size is not between 1 and 1000.
201
+ :return: A new params dict with pagination parameters added, or None if no params.
202
+ """
194
203
  if page is not None and page < 1:
195
204
  raise ValueError("page must be greater than 0")
196
205
  if page_size is not None and not 1 <= page_size <= 1000:
@@ -225,9 +234,7 @@ def _parse_paginated_response(payload: JSON) -> PaginatedResponse[Any]:
225
234
  if next_url is not None and not isinstance(next_url, str):
226
235
  raise ValueError("paginated response field 'next' must be a string or null")
227
236
  if previous_url is not None and not isinstance(previous_url, str):
228
- raise ValueError(
229
- "paginated response field 'previous' must be a string or null"
230
- )
237
+ raise ValueError("paginated response field 'previous' must be a string or null")
231
238
 
232
239
  return PaginatedResponse(
233
240
  count=int(payload["count"]),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modulector-sdk
3
- Version: 2.3.0
3
+ Version: 2.5.2
4
4
  Summary: Typed Python SDK for the Modulector API.
5
5
  Author: omicsdatascience
6
6
  License-Expression: MIT
@@ -25,6 +25,7 @@ Classifier: Typing :: Typed
25
25
  Requires-Python: >=3.10
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
+ Requires-Dist: mcp==1.27.2
28
29
  Requires-Dist: requests==2.31.0
29
30
  Dynamic: license-file
30
31
 
@@ -63,6 +64,46 @@ MODULECTOR_API_BASE_URL=https://your-modulector.example.org python script.py
63
64
  Every service function also accepts a `base_url` keyword argument for
64
65
  per-request overrides.
65
66
 
67
+ ## MCP server
68
+
69
+ The SDK also installs a Model Context Protocol server that exposes the same
70
+ service functions as LLM-callable tools.
71
+
72
+ For local stdio MCP clients:
73
+
74
+ ```bash
75
+ modulector-mcp
76
+ ```
77
+
78
+ Example client configuration:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "modulector": {
84
+ "command": "modulector-mcp"
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ For Streamable HTTP clients:
91
+
92
+ ```bash
93
+ modulector-mcp --transport streamable-http --host 127.0.0.1 --port 8000
94
+ ```
95
+
96
+ Then connect the client to `http://127.0.0.1:8000/mcp`.
97
+
98
+ The server includes tools for miRNA target interactions, miRNA details and
99
+ aliases, miRNA identifier resolution, methylation site lookup and annotation,
100
+ disease associations, drug associations, and PubMed news subscriptions. Use
101
+ `MODULECTOR_API_BASE_URL` or each tool's `base_url` argument to target a custom
102
+ Modulector deployment.
103
+
104
+ When deployed with the root Docker Compose stack, nginx proxies the MCP
105
+ Streamable HTTP endpoint at `https://<mydomain.com>/mcp`.
106
+
66
107
  ## Development
67
108
 
68
109
  Build this SDK from the `sdk/` directory:
@@ -2,11 +2,13 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/modulector_sdk/__init__.py
5
+ src/modulector_sdk/mcp_server.py
5
6
  src/modulector_sdk/py.typed
6
7
  src/modulector_sdk/services.py
7
8
  src/modulector_sdk/utils.py
8
9
  src/modulector_sdk.egg-info/PKG-INFO
9
10
  src/modulector_sdk.egg-info/SOURCES.txt
10
11
  src/modulector_sdk.egg-info/dependency_links.txt
12
+ src/modulector_sdk.egg-info/entry_points.txt
11
13
  src/modulector_sdk.egg-info/requires.txt
12
14
  src/modulector_sdk.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ modulector-mcp = modulector_sdk.mcp_server:main
@@ -1,44 +0,0 @@
1
- # Modulector SDK
2
-
3
- Typed Python SDK for the Modulector API. The package contains client helpers for
4
- querying miRNA target interactions, miRNA aliases, methylation sites, disease
5
- associations, drug associations, and PubMed subscriptions without installing the
6
- Django backend dependencies.
7
-
8
- ## Installation
9
-
10
- ```bash
11
- pip install modulector-sdk
12
- ```
13
-
14
- ## Usage
15
-
16
- ```python
17
- from modulector_sdk import get_mirna_details, get_mirna_target_interactions
18
-
19
- details = get_mirna_details("hsa-miR-21-5p")
20
- interactions = get_mirna_target_interactions(
21
- mirna="hsa-miR-21-5p",
22
- gene="PTEN",
23
- include_pubmeds=True,
24
- )
25
- ```
26
-
27
- Set `MODULECTOR_API_BASE_URL` to target a different Modulector deployment:
28
-
29
- ```bash
30
- MODULECTOR_API_BASE_URL=https://your-modulector.example.org python script.py
31
- ```
32
-
33
- Every service function also accepts a `base_url` keyword argument for
34
- per-request overrides.
35
-
36
- ## Development
37
-
38
- Build this SDK from the `sdk/` directory:
39
-
40
- ```bash
41
- uv build
42
- ```
43
-
44
- The generated source distribution and wheel are written to `sdk/dist/`.
File without changes
File without changes