scitex 2.15.2__py3-none-any.whl → 2.15.4__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 (77) hide show
  1. scitex/_mcp_resources/__init__.py +2 -0
  2. scitex/_mcp_resources/_scholar.py +148 -0
  3. scitex/_mcp_tools/scholar.py +50 -99
  4. scitex/_mcp_tools/social.py +15 -232
  5. scitex/_mcp_tools/writer.py +7 -17
  6. scitex/canvas/mcp_server.py +16 -3
  7. scitex/capture/mcp_server.py +16 -2
  8. scitex/cli/audio.py +90 -20
  9. scitex/cli/capture.py +120 -0
  10. scitex/cli/introspect.py +19 -12
  11. scitex/cli/plt.py +78 -21
  12. scitex/cli/scholar/__init__.py +160 -2
  13. scitex/cli/scholar/_crossref_scitex.py +25 -266
  14. scitex/cli/scholar/_openalex_scitex.py +55 -0
  15. scitex/cli/social.py +63 -22
  16. scitex/cli/stats.py +121 -3
  17. scitex/cli/writer.py +49 -423
  18. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +90 -0
  19. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +1571 -0
  20. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +6262 -0
  21. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +1274 -0
  22. scitex/dev/plt/data/mpl/dir_ax.txt +459 -0
  23. scitex/introspect/_list_api.py +5 -2
  24. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +2 -2
  25. scitex/scholar/__init__.py +14 -9
  26. scitex/scholar/_mcp/crossref_tool_schemas.py +133 -0
  27. scitex/scholar/_mcp/openalex_handlers.py +212 -0
  28. scitex/scholar/_mcp/openalex_tool_schemas.py +96 -0
  29. scitex/scholar/_mcp/tool_schemas.py +16 -1
  30. scitex/scholar/data/.gitkeep +0 -0
  31. scitex/scholar/data/README.md +44 -0
  32. scitex/scholar/data/bib_files/bibliography.bib +1952 -0
  33. scitex/scholar/data/bib_files/neurovista.bib +277 -0
  34. scitex/scholar/data/bib_files/neurovista_enriched.bib +441 -0
  35. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +441 -0
  36. scitex/scholar/data/bib_files/neurovista_processed.bib +338 -0
  37. scitex/scholar/data/bib_files/openaccess.bib +89 -0
  38. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +2178 -0
  39. scitex/scholar/data/bib_files/pac.bib +698 -0
  40. scitex/scholar/data/bib_files/pac_enriched.bib +1061 -0
  41. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  42. scitex/scholar/data/bib_files/pac_titles.txt +75 -0
  43. scitex/scholar/data/bib_files/paywalled.bib +98 -0
  44. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +58 -0
  45. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +87 -0
  46. scitex/scholar/data/bib_files/seizure_prediction.bib +694 -0
  47. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  48. scitex/scholar/data/bib_files/test_complete_enriched.bib +437 -0
  49. scitex/scholar/data/bib_files/test_final_enriched.bib +437 -0
  50. scitex/scholar/data/bib_files/test_seizure.bib +46 -0
  51. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  52. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  53. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  54. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  55. scitex/scholar/data/impact_factor.db +0 -0
  56. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +2 -2
  57. scitex/scholar/local_dbs/__init__.py +31 -0
  58. scitex/scholar/local_dbs/crossref_scitex.py +30 -0
  59. scitex/scholar/local_dbs/openalex_scitex.py +30 -0
  60. scitex/scholar/mcp_server.py +59 -4
  61. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +2 -2
  62. scitex/stats/mcp_server.py +16 -3
  63. scitex/template/mcp_server.py +16 -3
  64. scitex/ui/mcp_server.py +16 -3
  65. scitex/writer/__init__.py +43 -34
  66. {scitex-2.15.2.dist-info → scitex-2.15.4.dist-info}/METADATA +22 -3
  67. {scitex-2.15.2.dist-info → scitex-2.15.4.dist-info}/RECORD +70 -38
  68. scitex/scholar/crossref_scitex.py +0 -367
  69. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +0 -462
  70. scitex/scholar/url_finder/.tmp/open_url/README.md +0 -223
  71. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +0 -694
  72. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +0 -1160
  73. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +0 -344
  74. scitex/scholar/url_finder/.tmp/open_url/__init__.py +0 -24
  75. {scitex-2.15.2.dist-info → scitex-2.15.4.dist-info}/WHEEL +0 -0
  76. {scitex-2.15.2.dist-info → scitex-2.15.4.dist-info}/entry_points.txt +0 -0
  77. {scitex-2.15.2.dist-info → scitex-2.15.4.dist-info}/licenses/LICENSE +0 -0
@@ -20,6 +20,7 @@ from ._cheatsheet import register_cheatsheet_resources
20
20
  from ._figrecipe import register_figrecipe_resources
21
21
  from ._formats import register_format_resources
22
22
  from ._modules import register_module_resources
23
+ from ._scholar import register_scholar_resources
23
24
  from ._session import register_session_resources
24
25
 
25
26
  __all__ = ["register_resources"]
@@ -32,6 +33,7 @@ def register_resources(mcp) -> None:
32
33
  register_module_resources(mcp)
33
34
  register_format_resources(mcp)
34
35
  register_figrecipe_resources(mcp)
36
+ register_scholar_resources(mcp)
35
37
 
36
38
 
37
39
  # EOF
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-29
3
+ # File: src/scitex/_mcp_resources/_scholar.py
4
+ """Scholar library resources for FastMCP unified server.
5
+
6
+ Provides dynamic resources for:
7
+ - scholar://library/{project} - Project paper listings
8
+ - scholar://bibtex/{filename} - BibTeX file contents
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+
18
+ __all__ = ["register_scholar_resources"]
19
+
20
+ # Directory configuration
21
+ SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
22
+ SCITEX_SCHOLAR_DIR = SCITEX_BASE_DIR / "scholar"
23
+
24
+
25
+ def _get_scholar_dir() -> Path:
26
+ """Get the scholar data directory."""
27
+ SCITEX_SCHOLAR_DIR.mkdir(parents=True, exist_ok=True)
28
+ return SCITEX_SCHOLAR_DIR
29
+
30
+
31
+ def register_scholar_resources(mcp) -> None:
32
+ """Register scholar library resources with FastMCP server."""
33
+
34
+ @mcp.resource("scholar://library")
35
+ def list_library_projects() -> str:
36
+ """List all scholar library projects with paper counts."""
37
+ scholar_dir = _get_scholar_dir()
38
+ library_dir = scholar_dir / "library"
39
+
40
+ if not library_dir.exists():
41
+ return json.dumps({"projects": [], "total": 0}, indent=2)
42
+
43
+ projects = []
44
+ for project_dir in library_dir.iterdir():
45
+ if project_dir.is_dir() and not project_dir.name.startswith("."):
46
+ pdf_count = len(list(project_dir.rglob("*.pdf")))
47
+ metadata_count = len(list(project_dir.rglob("metadata.json")))
48
+ projects.append(
49
+ {
50
+ "name": project_dir.name,
51
+ "pdf_count": pdf_count,
52
+ "paper_count": metadata_count,
53
+ "uri": f"scholar://library/{project_dir.name}",
54
+ }
55
+ )
56
+
57
+ return json.dumps(
58
+ {
59
+ "projects": projects,
60
+ "total": len(projects),
61
+ "library_path": str(library_dir),
62
+ },
63
+ indent=2,
64
+ )
65
+
66
+ @mcp.resource("scholar://library/{project}")
67
+ def get_library_project(project: str) -> str:
68
+ """Get papers in a specific library project."""
69
+ library_dir = _get_scholar_dir() / "library" / project
70
+
71
+ if not library_dir.exists():
72
+ return json.dumps({"error": f"Project not found: {project}"}, indent=2)
73
+
74
+ metadata_files = list(library_dir.rglob("metadata.json"))
75
+ papers = []
76
+
77
+ for meta_file in metadata_files[:100]:
78
+ try:
79
+ with open(meta_file) as f:
80
+ meta = json.load(f)
81
+ pdf_exists = any(meta_file.parent.glob("*.pdf"))
82
+ papers.append(
83
+ {
84
+ "id": meta_file.parent.name,
85
+ "title": meta.get("title"),
86
+ "doi": meta.get("doi"),
87
+ "authors": meta.get("authors", [])[:3],
88
+ "year": meta.get("year"),
89
+ "has_pdf": pdf_exists,
90
+ }
91
+ )
92
+ except Exception:
93
+ pass
94
+
95
+ return json.dumps(
96
+ {
97
+ "project": project,
98
+ "paper_count": len(papers),
99
+ "papers": papers,
100
+ },
101
+ indent=2,
102
+ )
103
+
104
+ @mcp.resource("scholar://bibtex")
105
+ def list_bibtex_files() -> str:
106
+ """List recent BibTeX files in scholar directory."""
107
+ scholar_dir = _get_scholar_dir()
108
+ bib_files = []
109
+
110
+ for bib_file in sorted(
111
+ scholar_dir.rglob("*.bib"),
112
+ key=lambda p: p.stat().st_mtime,
113
+ reverse=True,
114
+ )[:20]:
115
+ mtime = datetime.fromtimestamp(bib_file.stat().st_mtime)
116
+ bib_files.append(
117
+ {
118
+ "name": bib_file.name,
119
+ "path": str(bib_file),
120
+ "modified": mtime.isoformat(),
121
+ "uri": f"scholar://bibtex/{bib_file.name}",
122
+ }
123
+ )
124
+
125
+ return json.dumps(
126
+ {
127
+ "bibtex_files": bib_files,
128
+ "total": len(bib_files),
129
+ },
130
+ indent=2,
131
+ )
132
+
133
+ @mcp.resource("scholar://bibtex/{filename}")
134
+ def get_bibtex_file(filename: str) -> str:
135
+ """Read a BibTeX file by name."""
136
+ scholar_dir = _get_scholar_dir()
137
+ bib_files = list(scholar_dir.rglob(filename))
138
+
139
+ if not bib_files:
140
+ return json.dumps({"error": f"BibTeX file not found: {filename}"}, indent=2)
141
+
142
+ with open(bib_files[0]) as f:
143
+ content = f.read()
144
+
145
+ return content
146
+
147
+
148
+ # EOF
@@ -6,7 +6,6 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import json
9
- from typing import List, Optional
10
9
 
11
10
 
12
11
  def _json(data: dict) -> str:
@@ -20,10 +19,10 @@ def register_scholar_tools(mcp) -> None:
20
19
  async def scholar_search_papers(
21
20
  query: str,
22
21
  limit: int = 20,
23
- year_min: Optional[int] = None,
24
- year_max: Optional[int] = None,
22
+ year_min: int | None = None,
23
+ year_max: int | None = None,
25
24
  search_mode: str = "local",
26
- sources: Optional[List[str]] = None,
25
+ sources: list[str] | None = None,
27
26
  ) -> str:
28
27
  """[scholar] Search for scientific papers. Supports local library search and external databases (CrossRef, Semantic Scholar, PubMed, arXiv, OpenAlex)."""
29
28
  from scitex.scholar._mcp.handlers import search_papers_handler
@@ -40,9 +39,9 @@ def register_scholar_tools(mcp) -> None:
40
39
 
41
40
  @mcp.tool()
42
41
  async def scholar_resolve_dois(
43
- titles: Optional[List[str]] = None,
44
- bibtex_path: Optional[str] = None,
45
- project: Optional[str] = None,
42
+ titles: list[str] | None = None,
43
+ bibtex_path: str | None = None,
44
+ project: str | None = None,
46
45
  resume: bool = True,
47
46
  ) -> str:
48
47
  """[scholar] Resolve DOIs from paper titles using Crossref API. Supports resumable operation for large batches."""
@@ -59,7 +58,7 @@ def register_scholar_tools(mcp) -> None:
59
58
  @mcp.tool()
60
59
  async def scholar_enrich_bibtex(
61
60
  bibtex_path: str,
62
- output_path: Optional[str] = None,
61
+ output_path: str | None = None,
63
62
  add_abstracts: bool = True,
64
63
  add_citations: bool = True,
65
64
  add_impact_factors: bool = True,
@@ -94,10 +93,10 @@ def register_scholar_tools(mcp) -> None:
94
93
 
95
94
  @mcp.tool()
96
95
  async def scholar_download_pdfs_batch(
97
- dois: Optional[List[str]] = None,
98
- bibtex_path: Optional[str] = None,
99
- project: Optional[str] = None,
100
- output_dir: Optional[str] = None,
96
+ dois: list[str] | None = None,
97
+ bibtex_path: str | None = None,
98
+ project: str | None = None,
99
+ output_dir: str | None = None,
101
100
  max_concurrent: int = 3,
102
101
  resume: bool = True,
103
102
  ) -> str:
@@ -116,7 +115,7 @@ def register_scholar_tools(mcp) -> None:
116
115
 
117
116
  @mcp.tool()
118
117
  async def scholar_get_library_status(
119
- project: Optional[str] = None,
118
+ project: str | None = None,
120
119
  include_details: bool = False,
121
120
  ) -> str:
122
121
  """[scholar] Get status of the paper library: download progress, missing PDFs, validation status."""
@@ -138,8 +137,8 @@ def register_scholar_tools(mcp) -> None:
138
137
 
139
138
  @mcp.tool()
140
139
  async def scholar_validate_pdfs(
141
- project: Optional[str] = None,
142
- pdf_paths: Optional[List[str]] = None,
140
+ project: str | None = None,
141
+ pdf_paths: list[str] | None = None,
143
142
  ) -> str:
144
143
  """[scholar] Validate PDF files in library for completeness and readability."""
145
144
  from scitex.scholar._mcp.handlers import validate_pdfs_handler
@@ -152,8 +151,8 @@ def register_scholar_tools(mcp) -> None:
152
151
 
153
152
  @mcp.tool()
154
153
  async def scholar_resolve_openurls(
155
- dois: List[str],
156
- resolver_url: Optional[str] = None,
154
+ dois: list[str],
155
+ resolver_url: str | None = None,
157
156
  resume: bool = True,
158
157
  ) -> str:
159
158
  """[scholar] Resolve publisher URLs via OpenURL resolver for institutional access."""
@@ -169,7 +168,7 @@ def register_scholar_tools(mcp) -> None:
169
168
  @mcp.tool()
170
169
  async def scholar_authenticate(
171
170
  method: str,
172
- institution: Optional[str] = None,
171
+ institution: str | None = None,
173
172
  force: bool = False,
174
173
  confirm: bool = False,
175
174
  ) -> str:
@@ -215,7 +214,7 @@ def register_scholar_tools(mcp) -> None:
215
214
  @mcp.tool()
216
215
  async def scholar_export_papers(
217
216
  output_path: str,
218
- project: Optional[str] = None,
217
+ project: str | None = None,
219
218
  format: str = "bibtex",
220
219
  filter_has_pdf: bool = False,
221
220
  ) -> str:
@@ -233,7 +232,7 @@ def register_scholar_tools(mcp) -> None:
233
232
  @mcp.tool()
234
233
  async def scholar_create_project(
235
234
  project_name: str,
236
- description: Optional[str] = None,
235
+ description: str | None = None,
237
236
  ) -> str:
238
237
  """[scholar] Create a new scholar project for organizing papers."""
239
238
  from scitex.scholar._mcp.handlers import create_project_handler
@@ -255,8 +254,8 @@ def register_scholar_tools(mcp) -> None:
255
254
  @mcp.tool()
256
255
  async def scholar_add_papers_to_project(
257
256
  project: str,
258
- dois: Optional[List[str]] = None,
259
- bibtex_path: Optional[str] = None,
257
+ dois: list[str] | None = None,
258
+ bibtex_path: str | None = None,
260
259
  ) -> str:
261
260
  """[scholar] Add papers to a project by DOI or from BibTeX file."""
262
261
  from scitex.scholar._mcp.handlers import add_papers_to_project_handler
@@ -270,14 +269,14 @@ def register_scholar_tools(mcp) -> None:
270
269
 
271
270
  @mcp.tool()
272
271
  async def scholar_parse_pdf_content(
273
- pdf_path: Optional[str] = None,
274
- doi: Optional[str] = None,
275
- project: Optional[str] = None,
272
+ pdf_path: str | None = None,
273
+ doi: str | None = None,
274
+ project: str | None = None,
276
275
  mode: str = "scientific",
277
276
  extract_sections: bool = True,
278
277
  extract_tables: bool = False,
279
278
  extract_images: bool = False,
280
- max_pages: Optional[int] = None,
279
+ max_pages: int | None = None,
281
280
  ) -> str:
282
281
  """[scholar] Parse PDF content to extract text, sections (IMRaD), tables, images, and metadata."""
283
282
  from scitex.scholar._mcp.handlers import parse_pdf_content_handler
@@ -297,14 +296,14 @@ def register_scholar_tools(mcp) -> None:
297
296
  # Job management tools (from job_handlers.py)
298
297
  @mcp.tool()
299
298
  async def scholar_fetch_papers(
300
- papers: Optional[List[str]] = None,
301
- bibtex_path: Optional[str] = None,
302
- project: Optional[str] = None,
303
- workers: Optional[int] = None,
299
+ papers: list[str] | None = None,
300
+ bibtex_path: str | None = None,
301
+ project: str | None = None,
302
+ workers: int | None = None,
304
303
  browser_mode: str = "stealth",
305
304
  chrome_profile: str = "system",
306
305
  force: bool = False,
307
- output: Optional[str] = None,
306
+ output: str | None = None,
308
307
  async_mode: bool = True,
309
308
  ) -> str:
310
309
  """[scholar] Fetch papers to your library. Supports async mode (default) which returns immediately with a job_id for tracking."""
@@ -325,7 +324,7 @@ def register_scholar_tools(mcp) -> None:
325
324
 
326
325
  @mcp.tool()
327
326
  async def scholar_list_jobs(
328
- status: Optional[str] = None,
327
+ status: str | None = None,
329
328
  limit: int = 20,
330
329
  ) -> str:
331
330
  """[scholar] List all background jobs with their status."""
@@ -369,79 +368,31 @@ def register_scholar_tools(mcp) -> None:
369
368
  result = await get_job_result_handler(job_id=job_id)
370
369
  return _json(result)
371
370
 
372
- # =========================================================================
373
- # CrossRef Tools (via crossref-local delegation - 167M+ papers)
374
- # =========================================================================
371
+ # Import crossref-local and openalex-local MCP servers
372
+ _import_local_db_servers(mcp)
375
373
 
376
- @mcp.tool()
377
- async def scholar_crossref_search(
378
- query: str,
379
- limit: int = 20,
380
- offset: int = 0,
381
- year_min: Optional[int] = None,
382
- year_max: Optional[int] = None,
383
- enrich: bool = False,
384
- ) -> str:
385
- """[scholar] Search CrossRef database (167M+ papers) via crossref-local. Fast full-text search with optional citation enrichment."""
386
- from scitex.scholar._mcp.crossref_handlers import crossref_search_handler
387
374
 
388
- result = await crossref_search_handler(
389
- query=query,
390
- limit=limit,
391
- offset=offset,
392
- year_min=year_min,
393
- year_max=year_max,
394
- enrich=enrich,
395
- )
396
- return _json(result)
375
+ def _import_local_db_servers(mcp) -> None:
376
+ """Mount crossref-local and openalex-local MCP servers if available.
397
377
 
398
- @mcp.tool()
399
- async def scholar_crossref_get(
400
- doi: str,
401
- include_citations: bool = False,
402
- include_references: bool = False,
403
- ) -> str:
404
- """[scholar] Get paper metadata by DOI from CrossRef database."""
405
- from scitex.scholar._mcp.crossref_handlers import crossref_get_handler
378
+ Uses fastmcp's mount() for automatic tool delegation.
379
+ Tools are prefixed: crossref_search, openalex_search, etc.
380
+ """
381
+ # Mount crossref-local MCP server (167M+ papers)
382
+ try:
383
+ from crossref_local.mcp_server import mcp as crossref_mcp
406
384
 
407
- result = await crossref_get_handler(
408
- doi=doi,
409
- include_citations=include_citations,
410
- include_references=include_references,
411
- )
412
- return _json(result)
385
+ mcp.mount(crossref_mcp, prefix="crossref")
386
+ except ImportError:
387
+ pass # crossref-local not installed
413
388
 
414
- @mcp.tool()
415
- async def scholar_crossref_count(query: str) -> str:
416
- """[scholar] Count papers matching a query in CrossRef database."""
417
- from scitex.scholar._mcp.crossref_handlers import crossref_count_handler
418
-
419
- result = await crossref_count_handler(query=query)
420
- return _json(result)
421
-
422
- @mcp.tool()
423
- async def scholar_crossref_citations(
424
- doi: str,
425
- direction: str = "citing",
426
- limit: int = 100,
427
- ) -> str:
428
- """[scholar] Get citation relationships (citing/cited papers) for a DOI."""
429
- from scitex.scholar._mcp.crossref_handlers import crossref_citations_handler
389
+ # Mount openalex-local MCP server (250M+ papers)
390
+ try:
391
+ from openalex_local._cli.mcp_server import mcp as openalex_mcp
430
392
 
431
- result = await crossref_citations_handler(
432
- doi=doi,
433
- direction=direction,
434
- limit=limit,
435
- )
436
- return _json(result)
437
-
438
- @mcp.tool()
439
- async def scholar_crossref_info() -> str:
440
- """[scholar] Get CrossRef database configuration and status info."""
441
- from scitex.scholar._mcp.crossref_handlers import crossref_info_handler
442
-
443
- result = await crossref_info_handler()
444
- return _json(result)
393
+ mcp.mount(openalex_mcp, prefix="openalex")
394
+ except ImportError:
395
+ pass # openalex-local not installed
445
396
 
446
397
 
447
398
  # EOF