scitex 2.15.3__py3-none-any.whl → 2.15.5__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.
@@ -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
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env python3
2
- # Timestamp: 2026-01-15
3
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/_mcp_tools/canvas.py
4
- """Canvas module tools for FastMCP unified server."""
2
+ # Timestamp: 2026-01-29
3
+ # File: src/scitex/_mcp_tools/canvas.py
4
+ """Canvas module tools for FastMCP unified server.
5
+
6
+ .. deprecated:: 2.16.0
7
+ Canvas tools are deprecated. Use figrecipe MCP tools instead:
8
+ - plt_compose for multi-panel composition
9
+ - plt_plot for creating figures
10
+ - plt_reproduce for reproducing from recipes
11
+
12
+ figrecipe is mounted automatically if installed.
13
+ """
5
14
 
6
15
  from __future__ import annotations
7
16
 
8
17
  import json
9
- from typing import Optional
10
18
 
11
19
 
12
20
  def _json(data: dict) -> str:
@@ -14,8 +22,14 @@ def _json(data: dict) -> str:
14
22
 
15
23
 
16
24
  def register_canvas_tools(mcp) -> None:
17
- """Register canvas tools with FastMCP server."""
25
+ """Register canvas tools with FastMCP server.
18
26
 
27
+ Note: These tools are deprecated. figrecipe is mounted for composition.
28
+ """
29
+ # Mount figrecipe MCP server if available (preferred)
30
+ _mount_figrecipe(mcp)
31
+
32
+ # Legacy canvas tools (deprecated, for backward compatibility)
19
33
  @mcp.tool()
20
34
  async def canvas_create_canvas(
21
35
  parent_dir: str,
@@ -23,7 +37,7 @@ def register_canvas_tools(mcp) -> None:
23
37
  width_mm: float = 180,
24
38
  height_mm: float = 120,
25
39
  ) -> str:
26
- """[canvas] Create a new paper figure canvas workspace."""
40
+ """[canvas][DEPRECATED] Create a canvas workspace. Use plt_compose instead."""
27
41
  from scitex.canvas._mcp.handlers import create_canvas_handler
28
42
 
29
43
  result = await create_canvas_handler(
@@ -32,6 +46,7 @@ def register_canvas_tools(mcp) -> None:
32
46
  width_mm=width_mm,
33
47
  height_mm=height_mm,
34
48
  )
49
+ result["_deprecated"] = "Use plt_compose from figrecipe instead"
35
50
  return _json(result)
36
51
 
37
52
  @mcp.tool()
@@ -44,9 +59,9 @@ def register_canvas_tools(mcp) -> None:
44
59
  y_mm: float = 0,
45
60
  width_mm: float = 50,
46
61
  height_mm: float = 50,
47
- label: Optional[str] = None,
62
+ label: str | None = None,
48
63
  ) -> str:
49
- """[canvas] Add a panel to an existing canvas from an image or plot."""
64
+ """[canvas][DEPRECATED] Add a panel to canvas. Use plt_compose instead."""
50
65
  from scitex.canvas._mcp.handlers import add_panel_handler
51
66
 
52
67
  result = await add_panel_handler(
@@ -60,23 +75,25 @@ def register_canvas_tools(mcp) -> None:
60
75
  height_mm=height_mm,
61
76
  label=label,
62
77
  )
78
+ result["_deprecated"] = "Use plt_compose from figrecipe instead"
63
79
  return _json(result)
64
80
 
65
81
  @mcp.tool()
66
82
  async def canvas_list_panels(parent_dir: str, canvas_name: str) -> str:
67
- """[canvas] List all panels in a canvas with their properties."""
83
+ """[canvas][DEPRECATED] List panels in a canvas."""
68
84
  from scitex.canvas._mcp.handlers import list_panels_handler
69
85
 
70
86
  result = await list_panels_handler(
71
87
  parent_dir=parent_dir, canvas_name=canvas_name
72
88
  )
89
+ result["_deprecated"] = "Use figrecipe instead"
73
90
  return _json(result)
74
91
 
75
92
  @mcp.tool()
76
93
  async def canvas_remove_panel(
77
94
  parent_dir: str, canvas_name: str, panel_name: str
78
95
  ) -> str:
79
- """[canvas] Remove a panel from a canvas."""
96
+ """[canvas][DEPRECATED] Remove a panel from canvas."""
80
97
  from scitex.canvas._mcp.handlers import remove_panel_handler
81
98
 
82
99
  result = await remove_panel_handler(
@@ -84,17 +101,18 @@ def register_canvas_tools(mcp) -> None:
84
101
  canvas_name=canvas_name,
85
102
  panel_name=panel_name,
86
103
  )
104
+ result["_deprecated"] = "Use figrecipe instead"
87
105
  return _json(result)
88
106
 
89
107
  @mcp.tool()
90
108
  async def canvas_export_canvas(
91
109
  parent_dir: str,
92
110
  canvas_name: str,
93
- output_path: Optional[str] = None,
94
- format: Optional[str] = None,
111
+ output_path: str | None = None,
112
+ format: str | None = None,
95
113
  dpi: int = 300,
96
114
  ) -> str:
97
- """[canvas] Export/render canvas to PNG, PDF, or SVG format."""
115
+ """[canvas][DEPRECATED] Export canvas to image. Use plt_compose instead."""
98
116
  from scitex.canvas._mcp.handlers import export_canvas_handler
99
117
 
100
118
  result = await export_canvas_handler(
@@ -104,25 +122,41 @@ def register_canvas_tools(mcp) -> None:
104
122
  format=format,
105
123
  dpi=dpi,
106
124
  )
125
+ result["_deprecated"] = "Use plt_compose from figrecipe instead"
107
126
  return _json(result)
108
127
 
109
128
  @mcp.tool()
110
129
  async def canvas_list_canvases(parent_dir: str) -> str:
111
- """[canvas] List all canvases in a directory."""
130
+ """[canvas][DEPRECATED] List canvases in a directory."""
112
131
  from scitex.canvas._mcp.handlers import list_canvases_handler
113
132
 
114
133
  result = await list_canvases_handler(parent_dir=parent_dir)
134
+ result["_deprecated"] = "Use figrecipe instead"
115
135
  return _json(result)
116
136
 
117
137
  @mcp.tool()
118
138
  async def canvas_canvas_exists(parent_dir: str, canvas_name: str) -> str:
119
- """[canvas] Check if a canvas exists."""
139
+ """[canvas][DEPRECATED] Check if a canvas exists."""
120
140
  from scitex.canvas._mcp.handlers import canvas_exists_handler
121
141
 
122
142
  result = await canvas_exists_handler(
123
143
  parent_dir=parent_dir, canvas_name=canvas_name
124
144
  )
145
+ result["_deprecated"] = "Use figrecipe instead"
125
146
  return _json(result)
126
147
 
127
148
 
149
+ def _mount_figrecipe(mcp) -> None:
150
+ """Mount figrecipe MCP server for composition tools.
151
+
152
+ Provides: plt_plot, plt_compose, plt_reproduce, plt_info, etc.
153
+ """
154
+ try:
155
+ from figrecipe._mcp.server import mcp as figrecipe_mcp
156
+
157
+ mcp.mount(figrecipe_mcp, prefix="plt")
158
+ except ImportError:
159
+ pass # figrecipe not installed
160
+
161
+
128
162
  # 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