scitex 2.11.0__py3-none-any.whl → 2.13.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.
Files changed (148) hide show
  1. scitex/__main__.py +24 -5
  2. scitex/__version__.py +1 -1
  3. scitex/_optional_deps.py +33 -0
  4. scitex/ai/classification/reporters/_ClassificationReporter.py +1 -1
  5. scitex/ai/classification/timeseries/_TimeSeriesBlockingSplit.py +2 -2
  6. scitex/ai/classification/timeseries/_TimeSeriesCalendarSplit.py +2 -2
  7. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +2 -2
  8. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +2 -2
  9. scitex/ai/classification/timeseries/_TimeSeriesStratifiedSplit.py +2 -2
  10. scitex/ai/classification/timeseries/_normalize_timestamp.py +1 -1
  11. scitex/ai/metrics/_calc_seizure_prediction_metrics.py +1 -1
  12. scitex/ai/plt/_plot_feature_importance.py +1 -1
  13. scitex/ai/plt/_plot_learning_curve.py +1 -1
  14. scitex/ai/plt/_plot_optuna_study.py +1 -1
  15. scitex/ai/plt/_plot_pre_rec_curve.py +1 -1
  16. scitex/ai/plt/_plot_roc_curve.py +1 -1
  17. scitex/ai/plt/_stx_conf_mat.py +1 -1
  18. scitex/ai/training/_LearningCurveLogger.py +1 -1
  19. scitex/audio/mcp_server.py +38 -8
  20. scitex/browser/automation/CookieHandler.py +1 -1
  21. scitex/browser/core/BrowserMixin.py +1 -1
  22. scitex/browser/core/ChromeProfileManager.py +1 -1
  23. scitex/browser/debugging/_browser_logger.py +1 -1
  24. scitex/browser/debugging/_highlight_element.py +1 -1
  25. scitex/browser/debugging/_show_grid.py +1 -1
  26. scitex/browser/interaction/click_center.py +1 -1
  27. scitex/browser/interaction/click_with_fallbacks.py +1 -1
  28. scitex/browser/interaction/close_popups.py +1 -1
  29. scitex/browser/interaction/fill_with_fallbacks.py +1 -1
  30. scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +1 -1
  31. scitex/browser/pdf/detect_chrome_pdf_viewer.py +1 -1
  32. scitex/browser/stealth/HumanBehavior.py +1 -1
  33. scitex/browser/stealth/StealthManager.py +1 -1
  34. scitex/canvas/_mcp_handlers.py +372 -0
  35. scitex/canvas/_mcp_tool_schemas.py +219 -0
  36. scitex/canvas/mcp_server.py +151 -0
  37. scitex/capture/mcp_server.py +41 -12
  38. scitex/cli/audio.py +233 -0
  39. scitex/cli/capture.py +307 -0
  40. scitex/cli/main.py +27 -4
  41. scitex/cli/repro.py +233 -0
  42. scitex/cli/resource.py +240 -0
  43. scitex/cli/stats.py +325 -0
  44. scitex/cli/template.py +236 -0
  45. scitex/cli/tex.py +286 -0
  46. scitex/cli/web.py +11 -12
  47. scitex/dev/__init__.py +3 -0
  48. scitex/dev/_pyproject.py +405 -0
  49. scitex/dev/plt/__init__.py +2 -2
  50. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  51. scitex/dev/plt/mpl/get_signatures.py +1 -1
  52. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  53. scitex/diagram/_mcp_handlers.py +400 -0
  54. scitex/diagram/_mcp_tool_schemas.py +157 -0
  55. scitex/diagram/mcp_server.py +151 -0
  56. scitex/dsp/_demo_sig.py +51 -5
  57. scitex/dsp/_mne.py +13 -2
  58. scitex/dsp/_modulation_index.py +15 -3
  59. scitex/dsp/_pac.py +23 -5
  60. scitex/dsp/_psd.py +16 -4
  61. scitex/dsp/_resample.py +24 -4
  62. scitex/dsp/_transform.py +16 -3
  63. scitex/dsp/add_noise.py +15 -1
  64. scitex/dsp/norm.py +17 -2
  65. scitex/dsp/reference.py +17 -1
  66. scitex/dsp/utils/_differential_bandpass_filters.py +20 -2
  67. scitex/dsp/utils/_zero_pad.py +18 -4
  68. scitex/dt/_normalize_timestamp.py +1 -1
  69. scitex/git/_session.py +1 -1
  70. scitex/io/_load_modules/_con.py +12 -1
  71. scitex/io/_load_modules/_eeg.py +12 -1
  72. scitex/io/_load_modules/_optuna.py +21 -63
  73. scitex/io/_load_modules/_torch.py +11 -3
  74. scitex/io/_save_modules/_optuna_study_as_csv_and_pngs.py +13 -2
  75. scitex/io/_save_modules/_torch.py +11 -3
  76. scitex/mcp_server.py +159 -0
  77. scitex/plt/_mcp_handlers.py +361 -0
  78. scitex/plt/_mcp_tool_schemas.py +169 -0
  79. scitex/plt/mcp_server.py +205 -0
  80. scitex/repro/README_RandomStateManager.md +3 -3
  81. scitex/repro/_RandomStateManager.py +14 -14
  82. scitex/repro/_gen_ID.py +1 -1
  83. scitex/repro/_gen_timestamp.py +1 -1
  84. scitex/repro/_hash_array.py +4 -4
  85. scitex/scholar/__main__.py +24 -2
  86. scitex/scholar/_mcp_handlers.py +685 -0
  87. scitex/scholar/_mcp_tool_schemas.py +339 -0
  88. scitex/scholar/docs/template.py +1 -1
  89. scitex/scholar/examples/07_storage_integration.py +1 -1
  90. scitex/scholar/impact_factor/jcr/ImpactFactorJCREngine.py +1 -1
  91. scitex/scholar/impact_factor/jcr/build_database.py +1 -1
  92. scitex/scholar/mcp_server.py +315 -0
  93. scitex/scholar/pdf_download/ScholarPDFDownloader.py +1 -1
  94. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +1 -1
  95. scitex/scholar/pipelines/ScholarPipelineParallel.py +1 -1
  96. scitex/scholar/pipelines/ScholarPipelineSingle.py +1 -1
  97. scitex/scholar/storage/PaperIO.py +1 -1
  98. scitex/session/README.md +4 -4
  99. scitex/session/__init__.py +1 -1
  100. scitex/session/_decorator.py +9 -9
  101. scitex/session/_lifecycle.py +5 -5
  102. scitex/session/template.py +1 -1
  103. scitex/stats/__main__.py +281 -0
  104. scitex/stats/_mcp_handlers.py +1191 -0
  105. scitex/stats/_mcp_tool_schemas.py +384 -0
  106. scitex/stats/correct/_correct_bonferroni.py +1 -1
  107. scitex/stats/correct/_correct_fdr.py +1 -1
  108. scitex/stats/correct/_correct_fdr_.py +1 -1
  109. scitex/stats/correct/_correct_holm.py +1 -1
  110. scitex/stats/correct/_correct_sidak.py +1 -1
  111. scitex/stats/effect_sizes/_cliffs_delta.py +1 -1
  112. scitex/stats/effect_sizes/_cohens_d.py +1 -1
  113. scitex/stats/effect_sizes/_epsilon_squared.py +1 -1
  114. scitex/stats/effect_sizes/_eta_squared.py +1 -1
  115. scitex/stats/effect_sizes/_prob_superiority.py +1 -1
  116. scitex/stats/mcp_server.py +405 -0
  117. scitex/stats/posthoc/_dunnett.py +1 -1
  118. scitex/stats/posthoc/_games_howell.py +1 -1
  119. scitex/stats/posthoc/_tukey_hsd.py +1 -1
  120. scitex/stats/power/_power.py +1 -1
  121. scitex/stats/utils/_effect_size.py +1 -1
  122. scitex/stats/utils/_formatters.py +1 -1
  123. scitex/stats/utils/_power.py +1 -1
  124. scitex/template/_mcp_handlers.py +259 -0
  125. scitex/template/_mcp_tool_schemas.py +112 -0
  126. scitex/template/mcp_server.py +186 -0
  127. scitex/utils/_verify_scitex_format.py +2 -2
  128. scitex/utils/template.py +1 -1
  129. scitex/web/__init__.py +12 -11
  130. scitex/web/_scraping.py +26 -265
  131. scitex/web/download_images.py +316 -0
  132. scitex/writer/Writer.py +1 -1
  133. scitex/writer/_clone_writer_project.py +1 -1
  134. scitex/writer/_validate_tree_structures.py +1 -1
  135. scitex/writer/dataclasses/config/_WriterConfig.py +1 -1
  136. scitex/writer/dataclasses/contents/_ManuscriptContents.py +1 -1
  137. scitex/writer/dataclasses/core/_Document.py +1 -1
  138. scitex/writer/dataclasses/core/_DocumentSection.py +1 -1
  139. scitex/writer/dataclasses/results/_CompilationResult.py +1 -1
  140. scitex/writer/dataclasses/results/_LaTeXIssue.py +1 -1
  141. scitex/writer/utils/.legacy_git_retry.py +7 -5
  142. scitex/writer/utils/_parse_latex_logs.py +1 -1
  143. {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/METADATA +431 -269
  144. {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/RECORD +147 -118
  145. scitex-2.13.0.dist-info/entry_points.txt +11 -0
  146. scitex-2.11.0.dist-info/entry_points.txt +0 -2
  147. {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/WHEEL +0 -0
  148. {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-08
3
+ # File: src/scitex/scholar/_mcp_tool_schemas.py
4
+ # ----------------------------------------
5
+
6
+ """Tool schemas for the scitex-scholar MCP server."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import mcp.types as types
11
+
12
+ __all__ = ["get_tool_schemas"]
13
+
14
+
15
+ def get_tool_schemas() -> list[types.Tool]:
16
+ """Return all tool schemas for the Scholar MCP server."""
17
+ return [
18
+ # Search tools
19
+ types.Tool(
20
+ name="search_papers",
21
+ description=(
22
+ "Search for scientific papers across multiple databases "
23
+ "(PubMed, Crossref, Semantic Scholar, etc.)"
24
+ ),
25
+ inputSchema={
26
+ "type": "object",
27
+ "properties": {
28
+ "query": {
29
+ "type": "string",
30
+ "description": "Search query string",
31
+ },
32
+ "sources": {
33
+ "type": "array",
34
+ "items": {"type": "string"},
35
+ "description": (
36
+ "Sources to search: pubmed, crossref, semantic_scholar, "
37
+ "google_scholar, arxiv"
38
+ ),
39
+ },
40
+ "limit": {
41
+ "type": "integer",
42
+ "description": "Maximum number of results",
43
+ "default": 20,
44
+ },
45
+ "year_min": {
46
+ "type": "integer",
47
+ "description": "Minimum publication year",
48
+ },
49
+ "year_max": {
50
+ "type": "integer",
51
+ "description": "Maximum publication year",
52
+ },
53
+ },
54
+ "required": ["query"],
55
+ },
56
+ ),
57
+ # DOI Resolution
58
+ types.Tool(
59
+ name="resolve_dois",
60
+ description=(
61
+ "Resolve DOIs from paper titles using Crossref API. "
62
+ "Supports resumable operation for large batches."
63
+ ),
64
+ inputSchema={
65
+ "type": "object",
66
+ "properties": {
67
+ "bibtex_path": {
68
+ "type": "string",
69
+ "description": "Path to BibTeX file to resolve DOIs for",
70
+ },
71
+ "titles": {
72
+ "type": "array",
73
+ "items": {"type": "string"},
74
+ "description": "List of paper titles to resolve DOIs for",
75
+ },
76
+ "resume": {
77
+ "type": "boolean",
78
+ "description": "Resume from previous progress",
79
+ "default": True,
80
+ },
81
+ "project": {
82
+ "type": "string",
83
+ "description": "Project name for organizing results",
84
+ },
85
+ },
86
+ },
87
+ ),
88
+ # BibTeX Enrichment
89
+ types.Tool(
90
+ name="enrich_bibtex",
91
+ description=(
92
+ "Enrich BibTeX entries with metadata: DOIs, abstracts, "
93
+ "citation counts, impact factors"
94
+ ),
95
+ inputSchema={
96
+ "type": "object",
97
+ "properties": {
98
+ "bibtex_path": {
99
+ "type": "string",
100
+ "description": "Path to BibTeX file to enrich",
101
+ },
102
+ "output_path": {
103
+ "type": "string",
104
+ "description": "Output path for enriched BibTeX (optional)",
105
+ },
106
+ "add_abstracts": {
107
+ "type": "boolean",
108
+ "description": "Add missing abstracts",
109
+ "default": True,
110
+ },
111
+ "add_citations": {
112
+ "type": "boolean",
113
+ "description": "Add citation counts",
114
+ "default": True,
115
+ },
116
+ "add_impact_factors": {
117
+ "type": "boolean",
118
+ "description": "Add journal impact factors",
119
+ "default": True,
120
+ },
121
+ },
122
+ "required": ["bibtex_path"],
123
+ },
124
+ ),
125
+ # PDF Download
126
+ types.Tool(
127
+ name="download_pdf",
128
+ description=(
129
+ "Download a PDF for a paper using DOI. Supports multiple strategies: "
130
+ "direct, publisher, open access repositories."
131
+ ),
132
+ inputSchema={
133
+ "type": "object",
134
+ "properties": {
135
+ "doi": {
136
+ "type": "string",
137
+ "description": "DOI of the paper to download",
138
+ },
139
+ "output_dir": {
140
+ "type": "string",
141
+ "description": "Directory to save PDF",
142
+ "default": "./pdfs",
143
+ },
144
+ "auth_method": {
145
+ "type": "string",
146
+ "description": "Authentication method: openathens, shibboleth, none",
147
+ "enum": ["openathens", "shibboleth", "none"],
148
+ "default": "none",
149
+ },
150
+ "use_browser": {
151
+ "type": "boolean",
152
+ "description": "Use browser-based download for paywalled content",
153
+ "default": False,
154
+ },
155
+ },
156
+ "required": ["doi"],
157
+ },
158
+ ),
159
+ # Batch PDF Download
160
+ types.Tool(
161
+ name="download_pdfs_batch",
162
+ description=(
163
+ "Download PDFs for multiple papers with progress tracking. "
164
+ "Supports resumable operation."
165
+ ),
166
+ inputSchema={
167
+ "type": "object",
168
+ "properties": {
169
+ "dois": {
170
+ "type": "array",
171
+ "items": {"type": "string"},
172
+ "description": "List of DOIs to download",
173
+ },
174
+ "bibtex_path": {
175
+ "type": "string",
176
+ "description": "Path to BibTeX file (alternative to dois)",
177
+ },
178
+ "project": {
179
+ "type": "string",
180
+ "description": "Project name for organizing downloads",
181
+ },
182
+ "output_dir": {
183
+ "type": "string",
184
+ "description": "Directory to save PDFs",
185
+ },
186
+ "max_concurrent": {
187
+ "type": "integer",
188
+ "description": "Maximum concurrent downloads",
189
+ "default": 3,
190
+ },
191
+ "resume": {
192
+ "type": "boolean",
193
+ "description": "Resume from previous progress",
194
+ "default": True,
195
+ },
196
+ },
197
+ },
198
+ ),
199
+ # Library Status
200
+ types.Tool(
201
+ name="get_library_status",
202
+ description=(
203
+ "Get status of the paper library: download progress, "
204
+ "missing PDFs, validation status"
205
+ ),
206
+ inputSchema={
207
+ "type": "object",
208
+ "properties": {
209
+ "project": {
210
+ "type": "string",
211
+ "description": "Project name to check (optional)",
212
+ },
213
+ "include_details": {
214
+ "type": "boolean",
215
+ "description": "Include detailed per-paper status",
216
+ "default": False,
217
+ },
218
+ },
219
+ },
220
+ ),
221
+ # Parse BibTeX
222
+ types.Tool(
223
+ name="parse_bibtex",
224
+ description="Parse a BibTeX file and return paper objects",
225
+ inputSchema={
226
+ "type": "object",
227
+ "properties": {
228
+ "bibtex_path": {
229
+ "type": "string",
230
+ "description": "Path to BibTeX file",
231
+ },
232
+ },
233
+ "required": ["bibtex_path"],
234
+ },
235
+ ),
236
+ # Validate PDFs
237
+ types.Tool(
238
+ name="validate_pdfs",
239
+ description=(
240
+ "Validate PDF files in library for completeness and readability"
241
+ ),
242
+ inputSchema={
243
+ "type": "object",
244
+ "properties": {
245
+ "project": {
246
+ "type": "string",
247
+ "description": "Project name to validate",
248
+ },
249
+ "pdf_paths": {
250
+ "type": "array",
251
+ "items": {"type": "string"},
252
+ "description": "Specific PDF paths to validate",
253
+ },
254
+ },
255
+ },
256
+ ),
257
+ # OpenURL Resolution
258
+ types.Tool(
259
+ name="resolve_openurls",
260
+ description=(
261
+ "Resolve publisher URLs via OpenURL resolver for institutional access"
262
+ ),
263
+ inputSchema={
264
+ "type": "object",
265
+ "properties": {
266
+ "dois": {
267
+ "type": "array",
268
+ "items": {"type": "string"},
269
+ "description": "DOIs to resolve OpenURLs for",
270
+ },
271
+ "resolver_url": {
272
+ "type": "string",
273
+ "description": "OpenURL resolver URL (uses default if not specified)",
274
+ },
275
+ "resume": {
276
+ "type": "boolean",
277
+ "description": "Resume from previous progress",
278
+ "default": True,
279
+ },
280
+ },
281
+ "required": ["dois"],
282
+ },
283
+ ),
284
+ # Authentication
285
+ types.Tool(
286
+ name="authenticate",
287
+ description=(
288
+ "Authenticate with institutional access (OpenAthens, Shibboleth)"
289
+ ),
290
+ inputSchema={
291
+ "type": "object",
292
+ "properties": {
293
+ "method": {
294
+ "type": "string",
295
+ "description": "Authentication method",
296
+ "enum": ["openathens", "shibboleth"],
297
+ },
298
+ "institution": {
299
+ "type": "string",
300
+ "description": "Institution identifier (e.g., 'unimelb')",
301
+ },
302
+ },
303
+ "required": ["method"],
304
+ },
305
+ ),
306
+ # Export to formats
307
+ types.Tool(
308
+ name="export_papers",
309
+ description="Export papers to various formats (BibTeX, RIS, JSON, CSV)",
310
+ inputSchema={
311
+ "type": "object",
312
+ "properties": {
313
+ "project": {
314
+ "type": "string",
315
+ "description": "Project name to export",
316
+ },
317
+ "output_path": {
318
+ "type": "string",
319
+ "description": "Output file path",
320
+ },
321
+ "format": {
322
+ "type": "string",
323
+ "description": "Export format",
324
+ "enum": ["bibtex", "ris", "json", "csv"],
325
+ "default": "bibtex",
326
+ },
327
+ "filter_has_pdf": {
328
+ "type": "boolean",
329
+ "description": "Only export papers with downloaded PDFs",
330
+ "default": False,
331
+ },
332
+ },
333
+ "required": ["output_path"],
334
+ },
335
+ ),
336
+ ]
337
+
338
+
339
+ # EOF
@@ -96,7 +96,7 @@ def run_main() -> None:
96
96
 
97
97
  args = parse_args()
98
98
 
99
- CONFIG, sys.stdout, sys.stderr, plt, CC, rng_manager = stx.session.start(
99
+ CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
100
100
  sys,
101
101
  plt,
102
102
  args=args,
@@ -227,7 +227,7 @@ def run_main() -> None:
227
227
 
228
228
  args = parse_args()
229
229
 
230
- CONFIG, sys.stdout, sys.stderr, plt, CC, rng_manager = stx.session.start(
230
+ CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
231
231
  sys,
232
232
  plt,
233
233
  args=args,
@@ -266,7 +266,7 @@ def run_main() -> None:
266
266
 
267
267
  args = parse_args()
268
268
 
269
- CONFIG, sys.stdout, sys.stderr, plt, CC, rng_manager = stx.session.start(
269
+ CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
270
270
  sys,
271
271
  plt,
272
272
  args=args,
@@ -234,7 +234,7 @@ def run_main() -> None:
234
234
 
235
235
  args = parse_args()
236
236
 
237
- CONFIG, sys.stdout, sys.stderr, plt, CC, rng_manager = stx.session.start(
237
+ CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
238
238
  sys,
239
239
  plt,
240
240
  args=args,
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-08
3
+ # File: src/scitex/scholar/mcp_server.py
4
+ # ----------------------------------------
5
+
6
+ """
7
+ MCP Server for SciTeX Scholar - Scientific Literature Management
8
+
9
+ Provides tools for:
10
+ - Searching papers across multiple databases
11
+ - Resolving DOIs from paper titles
12
+ - Enriching BibTeX with metadata
13
+ - Downloading PDFs with institutional access
14
+ - Managing paper libraries
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import os
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+
24
+ # Graceful MCP dependency handling
25
+ try:
26
+ import mcp.types as types
27
+ from mcp.server import NotificationOptions, Server
28
+ from mcp.server.models import InitializationOptions
29
+ from mcp.server.stdio import stdio_server
30
+
31
+ MCP_AVAILABLE = True
32
+ except ImportError:
33
+ MCP_AVAILABLE = False
34
+ types = None # type: ignore
35
+ Server = None # type: ignore
36
+ NotificationOptions = None # type: ignore
37
+ InitializationOptions = None # type: ignore
38
+ stdio_server = None # type: ignore
39
+
40
+ __all__ = ["ScholarServer", "main", "MCP_AVAILABLE"]
41
+
42
+ # Directory configuration
43
+ SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
44
+ SCITEX_SCHOLAR_DIR = SCITEX_BASE_DIR / "scholar"
45
+
46
+
47
+ def get_scholar_dir() -> Path:
48
+ """Get the scholar data directory."""
49
+ SCITEX_SCHOLAR_DIR.mkdir(parents=True, exist_ok=True)
50
+ return SCITEX_SCHOLAR_DIR
51
+
52
+
53
+ class ScholarServer:
54
+ """MCP Server for Scientific Literature Management."""
55
+
56
+ def __init__(self):
57
+ self.server = Server("scitex-scholar")
58
+ self._scholar_instance = None
59
+ self.setup_handlers()
60
+
61
+ @property
62
+ def scholar(self):
63
+ """Lazy-load Scholar instance."""
64
+ if self._scholar_instance is None:
65
+ try:
66
+ from scitex.scholar import Scholar
67
+
68
+ self._scholar_instance = Scholar()
69
+ except ImportError as e:
70
+ raise RuntimeError(f"Scholar module not available: {e}")
71
+ return self._scholar_instance
72
+
73
+ def setup_handlers(self):
74
+ """Set up MCP server handlers."""
75
+ from ._mcp_handlers import (
76
+ authenticate_handler,
77
+ download_pdf_handler,
78
+ download_pdfs_batch_handler,
79
+ enrich_bibtex_handler,
80
+ export_papers_handler,
81
+ get_library_status_handler,
82
+ parse_bibtex_handler,
83
+ resolve_dois_handler,
84
+ resolve_openurls_handler,
85
+ search_papers_handler,
86
+ validate_pdfs_handler,
87
+ )
88
+ from ._mcp_tool_schemas import get_tool_schemas
89
+
90
+ @self.server.list_tools()
91
+ async def handle_list_tools():
92
+ return get_tool_schemas()
93
+
94
+ @self.server.call_tool()
95
+ async def handle_call_tool(name: str, arguments: dict):
96
+ # Search tools
97
+ if name == "search_papers":
98
+ return await self._wrap_result(search_papers_handler(**arguments))
99
+
100
+ # DOI Resolution
101
+ elif name == "resolve_dois":
102
+ return await self._wrap_result(resolve_dois_handler(**arguments))
103
+
104
+ # BibTeX Enrichment
105
+ elif name == "enrich_bibtex":
106
+ return await self._wrap_result(enrich_bibtex_handler(**arguments))
107
+
108
+ # PDF Download
109
+ elif name == "download_pdf":
110
+ return await self._wrap_result(download_pdf_handler(**arguments))
111
+
112
+ elif name == "download_pdfs_batch":
113
+ return await self._wrap_result(download_pdfs_batch_handler(**arguments))
114
+
115
+ # Library Status
116
+ elif name == "get_library_status":
117
+ return await self._wrap_result(get_library_status_handler(**arguments))
118
+
119
+ # Parse BibTeX
120
+ elif name == "parse_bibtex":
121
+ return await self._wrap_result(parse_bibtex_handler(**arguments))
122
+
123
+ # Validate PDFs
124
+ elif name == "validate_pdfs":
125
+ return await self._wrap_result(validate_pdfs_handler(**arguments))
126
+
127
+ # OpenURL Resolution
128
+ elif name == "resolve_openurls":
129
+ return await self._wrap_result(resolve_openurls_handler(**arguments))
130
+
131
+ # Authentication
132
+ elif name == "authenticate":
133
+ return await self._wrap_result(authenticate_handler(**arguments))
134
+
135
+ # Export
136
+ elif name == "export_papers":
137
+ return await self._wrap_result(export_papers_handler(**arguments))
138
+
139
+ else:
140
+ raise ValueError(f"Unknown tool: {name}")
141
+
142
+ @self.server.list_resources()
143
+ async def handle_list_resources():
144
+ """List available library resources."""
145
+ scholar_dir = get_scholar_dir()
146
+ library_dir = scholar_dir / "library"
147
+
148
+ if not library_dir.exists():
149
+ return []
150
+
151
+ resources = []
152
+
153
+ # List projects as resources
154
+ for project_dir in library_dir.iterdir():
155
+ if project_dir.is_dir() and not project_dir.name.startswith("."):
156
+ pdf_count = len(list(project_dir.rglob("*.pdf")))
157
+ resources.append(
158
+ types.Resource(
159
+ uri=f"scholar://library/{project_dir.name}",
160
+ name=f"Library: {project_dir.name}",
161
+ description=f"Paper library with {pdf_count} PDFs",
162
+ mimeType="application/json",
163
+ )
164
+ )
165
+
166
+ # List recent BibTeX files
167
+ for bib_file in sorted(
168
+ scholar_dir.rglob("*.bib"),
169
+ key=lambda p: p.stat().st_mtime,
170
+ reverse=True,
171
+ )[:10]:
172
+ mtime = datetime.fromtimestamp(bib_file.stat().st_mtime)
173
+ resources.append(
174
+ types.Resource(
175
+ uri=f"scholar://bibtex/{bib_file.name}",
176
+ name=bib_file.name,
177
+ description=f"BibTeX from {mtime.strftime('%Y-%m-%d %H:%M')}",
178
+ mimeType="application/x-bibtex",
179
+ )
180
+ )
181
+
182
+ return resources
183
+
184
+ @self.server.read_resource()
185
+ async def handle_read_resource(uri: str):
186
+ """Read a library resource."""
187
+ import json
188
+
189
+ if uri.startswith("scholar://library/"):
190
+ project_name = uri.replace("scholar://library/", "")
191
+ library_dir = get_scholar_dir() / "library" / project_name
192
+
193
+ if not library_dir.exists():
194
+ raise ValueError(f"Project not found: {project_name}")
195
+
196
+ # Gather project info
197
+ metadata_files = list(library_dir.rglob("metadata.json"))
198
+ papers = []
199
+
200
+ for meta_file in metadata_files[:100]:
201
+ try:
202
+ with open(meta_file) as f:
203
+ meta = json.load(f)
204
+ pdf_exists = any(
205
+ (meta_file.parent / f).exists()
206
+ for f in meta_file.parent.glob("*.pdf")
207
+ )
208
+ papers.append(
209
+ {
210
+ "id": meta_file.parent.name,
211
+ "title": meta.get("title"),
212
+ "doi": meta.get("doi"),
213
+ "has_pdf": pdf_exists,
214
+ }
215
+ )
216
+ except Exception:
217
+ pass
218
+
219
+ content = json.dumps(
220
+ {
221
+ "project": project_name,
222
+ "paper_count": len(papers),
223
+ "papers": papers,
224
+ },
225
+ indent=2,
226
+ )
227
+
228
+ return types.TextResourceContents(
229
+ uri=uri,
230
+ mimeType="application/json",
231
+ text=content,
232
+ )
233
+
234
+ elif uri.startswith("scholar://bibtex/"):
235
+ filename = uri.replace("scholar://bibtex/", "")
236
+ bib_files = list(get_scholar_dir().rglob(filename))
237
+
238
+ if not bib_files:
239
+ raise ValueError(f"BibTeX file not found: {filename}")
240
+
241
+ with open(bib_files[0]) as f:
242
+ content = f.read()
243
+
244
+ return types.TextResourceContents(
245
+ uri=uri,
246
+ mimeType="application/x-bibtex",
247
+ text=content,
248
+ )
249
+
250
+ else:
251
+ raise ValueError(f"Unknown resource URI: {uri}")
252
+
253
+ async def _wrap_result(self, coro):
254
+ """Wrap handler result as MCP TextContent."""
255
+ import json
256
+
257
+ try:
258
+ result = await coro
259
+ return [
260
+ types.TextContent(
261
+ type="text",
262
+ text=json.dumps(result, indent=2, default=str),
263
+ )
264
+ ]
265
+ except Exception as e:
266
+ return [
267
+ types.TextContent(
268
+ type="text",
269
+ text=json.dumps({"success": False, "error": str(e)}, indent=2),
270
+ )
271
+ ]
272
+
273
+
274
+ async def _run_server():
275
+ """Run the MCP server (internal)."""
276
+ server = ScholarServer()
277
+ async with stdio_server() as (read_stream, write_stream):
278
+ await server.server.run(
279
+ read_stream,
280
+ write_stream,
281
+ InitializationOptions(
282
+ server_name="scitex-scholar",
283
+ server_version="0.1.0",
284
+ capabilities=server.server.get_capabilities(
285
+ notification_options=NotificationOptions(),
286
+ experimental_capabilities={},
287
+ ),
288
+ ),
289
+ )
290
+
291
+
292
+ def main():
293
+ """Main entry point for the MCP server."""
294
+ if not MCP_AVAILABLE:
295
+ import sys
296
+
297
+ print("=" * 60)
298
+ print("MCP Server 'scitex-scholar' requires the 'mcp' package.")
299
+ print()
300
+ print("Install with:")
301
+ print(" pip install mcp")
302
+ print()
303
+ print("Or install scitex with MCP support:")
304
+ print(" pip install scitex[mcp]")
305
+ print("=" * 60)
306
+ sys.exit(1)
307
+
308
+ asyncio.run(_run_server())
309
+
310
+
311
+ if __name__ == "__main__":
312
+ main()
313
+
314
+
315
+ # EOF