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.
- scitex/__main__.py +24 -5
- scitex/__version__.py +1 -1
- scitex/_optional_deps.py +33 -0
- scitex/ai/classification/reporters/_ClassificationReporter.py +1 -1
- scitex/ai/classification/timeseries/_TimeSeriesBlockingSplit.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesCalendarSplit.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesStratifiedSplit.py +2 -2
- scitex/ai/classification/timeseries/_normalize_timestamp.py +1 -1
- scitex/ai/metrics/_calc_seizure_prediction_metrics.py +1 -1
- scitex/ai/plt/_plot_feature_importance.py +1 -1
- scitex/ai/plt/_plot_learning_curve.py +1 -1
- scitex/ai/plt/_plot_optuna_study.py +1 -1
- scitex/ai/plt/_plot_pre_rec_curve.py +1 -1
- scitex/ai/plt/_plot_roc_curve.py +1 -1
- scitex/ai/plt/_stx_conf_mat.py +1 -1
- scitex/ai/training/_LearningCurveLogger.py +1 -1
- scitex/audio/mcp_server.py +38 -8
- scitex/browser/automation/CookieHandler.py +1 -1
- scitex/browser/core/BrowserMixin.py +1 -1
- scitex/browser/core/ChromeProfileManager.py +1 -1
- scitex/browser/debugging/_browser_logger.py +1 -1
- scitex/browser/debugging/_highlight_element.py +1 -1
- scitex/browser/debugging/_show_grid.py +1 -1
- scitex/browser/interaction/click_center.py +1 -1
- scitex/browser/interaction/click_with_fallbacks.py +1 -1
- scitex/browser/interaction/close_popups.py +1 -1
- scitex/browser/interaction/fill_with_fallbacks.py +1 -1
- scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +1 -1
- scitex/browser/pdf/detect_chrome_pdf_viewer.py +1 -1
- scitex/browser/stealth/HumanBehavior.py +1 -1
- scitex/browser/stealth/StealthManager.py +1 -1
- scitex/canvas/_mcp_handlers.py +372 -0
- scitex/canvas/_mcp_tool_schemas.py +219 -0
- scitex/canvas/mcp_server.py +151 -0
- scitex/capture/mcp_server.py +41 -12
- scitex/cli/audio.py +233 -0
- scitex/cli/capture.py +307 -0
- scitex/cli/main.py +27 -4
- scitex/cli/repro.py +233 -0
- scitex/cli/resource.py +240 -0
- scitex/cli/stats.py +325 -0
- scitex/cli/template.py +236 -0
- scitex/cli/tex.py +286 -0
- scitex/cli/web.py +11 -12
- scitex/dev/__init__.py +3 -0
- scitex/dev/_pyproject.py +405 -0
- scitex/dev/plt/__init__.py +2 -2
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/diagram/_mcp_handlers.py +400 -0
- scitex/diagram/_mcp_tool_schemas.py +157 -0
- scitex/diagram/mcp_server.py +151 -0
- scitex/dsp/_demo_sig.py +51 -5
- scitex/dsp/_mne.py +13 -2
- scitex/dsp/_modulation_index.py +15 -3
- scitex/dsp/_pac.py +23 -5
- scitex/dsp/_psd.py +16 -4
- scitex/dsp/_resample.py +24 -4
- scitex/dsp/_transform.py +16 -3
- scitex/dsp/add_noise.py +15 -1
- scitex/dsp/norm.py +17 -2
- scitex/dsp/reference.py +17 -1
- scitex/dsp/utils/_differential_bandpass_filters.py +20 -2
- scitex/dsp/utils/_zero_pad.py +18 -4
- scitex/dt/_normalize_timestamp.py +1 -1
- scitex/git/_session.py +1 -1
- scitex/io/_load_modules/_con.py +12 -1
- scitex/io/_load_modules/_eeg.py +12 -1
- scitex/io/_load_modules/_optuna.py +21 -63
- scitex/io/_load_modules/_torch.py +11 -3
- scitex/io/_save_modules/_optuna_study_as_csv_and_pngs.py +13 -2
- scitex/io/_save_modules/_torch.py +11 -3
- scitex/mcp_server.py +159 -0
- scitex/plt/_mcp_handlers.py +361 -0
- scitex/plt/_mcp_tool_schemas.py +169 -0
- scitex/plt/mcp_server.py +205 -0
- scitex/repro/README_RandomStateManager.md +3 -3
- scitex/repro/_RandomStateManager.py +14 -14
- scitex/repro/_gen_ID.py +1 -1
- scitex/repro/_gen_timestamp.py +1 -1
- scitex/repro/_hash_array.py +4 -4
- scitex/scholar/__main__.py +24 -2
- scitex/scholar/_mcp_handlers.py +685 -0
- scitex/scholar/_mcp_tool_schemas.py +339 -0
- scitex/scholar/docs/template.py +1 -1
- scitex/scholar/examples/07_storage_integration.py +1 -1
- scitex/scholar/impact_factor/jcr/ImpactFactorJCREngine.py +1 -1
- scitex/scholar/impact_factor/jcr/build_database.py +1 -1
- scitex/scholar/mcp_server.py +315 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +1 -1
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +1 -1
- scitex/scholar/pipelines/ScholarPipelineParallel.py +1 -1
- scitex/scholar/pipelines/ScholarPipelineSingle.py +1 -1
- scitex/scholar/storage/PaperIO.py +1 -1
- scitex/session/README.md +4 -4
- scitex/session/__init__.py +1 -1
- scitex/session/_decorator.py +9 -9
- scitex/session/_lifecycle.py +5 -5
- scitex/session/template.py +1 -1
- scitex/stats/__main__.py +281 -0
- scitex/stats/_mcp_handlers.py +1191 -0
- scitex/stats/_mcp_tool_schemas.py +384 -0
- scitex/stats/correct/_correct_bonferroni.py +1 -1
- scitex/stats/correct/_correct_fdr.py +1 -1
- scitex/stats/correct/_correct_fdr_.py +1 -1
- scitex/stats/correct/_correct_holm.py +1 -1
- scitex/stats/correct/_correct_sidak.py +1 -1
- scitex/stats/effect_sizes/_cliffs_delta.py +1 -1
- scitex/stats/effect_sizes/_cohens_d.py +1 -1
- scitex/stats/effect_sizes/_epsilon_squared.py +1 -1
- scitex/stats/effect_sizes/_eta_squared.py +1 -1
- scitex/stats/effect_sizes/_prob_superiority.py +1 -1
- scitex/stats/mcp_server.py +405 -0
- scitex/stats/posthoc/_dunnett.py +1 -1
- scitex/stats/posthoc/_games_howell.py +1 -1
- scitex/stats/posthoc/_tukey_hsd.py +1 -1
- scitex/stats/power/_power.py +1 -1
- scitex/stats/utils/_effect_size.py +1 -1
- scitex/stats/utils/_formatters.py +1 -1
- scitex/stats/utils/_power.py +1 -1
- scitex/template/_mcp_handlers.py +259 -0
- scitex/template/_mcp_tool_schemas.py +112 -0
- scitex/template/mcp_server.py +186 -0
- scitex/utils/_verify_scitex_format.py +2 -2
- scitex/utils/template.py +1 -1
- scitex/web/__init__.py +12 -11
- scitex/web/_scraping.py +26 -265
- scitex/web/download_images.py +316 -0
- scitex/writer/Writer.py +1 -1
- scitex/writer/_clone_writer_project.py +1 -1
- scitex/writer/_validate_tree_structures.py +1 -1
- scitex/writer/dataclasses/config/_WriterConfig.py +1 -1
- scitex/writer/dataclasses/contents/_ManuscriptContents.py +1 -1
- scitex/writer/dataclasses/core/_Document.py +1 -1
- scitex/writer/dataclasses/core/_DocumentSection.py +1 -1
- scitex/writer/dataclasses/results/_CompilationResult.py +1 -1
- scitex/writer/dataclasses/results/_LaTeXIssue.py +1 -1
- scitex/writer/utils/.legacy_git_retry.py +7 -5
- scitex/writer/utils/_parse_latex_logs.py +1 -1
- {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/METADATA +431 -269
- {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/RECORD +147 -118
- scitex-2.13.0.dist-info/entry_points.txt +11 -0
- scitex-2.11.0.dist-info/entry_points.txt +0 -2
- {scitex-2.11.0.dist-info → scitex-2.13.0.dist-info}/WHEEL +0 -0
- {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
|
scitex/scholar/docs/template.py
CHANGED
|
@@ -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
|