scholarx 2.3.3__tar.gz → 2.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {scholarx-2.3.3/scholarx.egg-info → scholarx-2.3.4}/PKG-INFO +5 -2
  2. {scholarx-2.3.3 → scholarx-2.3.4}/README.md +4 -1
  3. {scholarx-2.3.3 → scholarx-2.3.4}/pyproject.toml +1 -1
  4. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/mcp_server.py +62 -1
  5. {scholarx-2.3.3 → scholarx-2.3.4/scholarx.egg-info}/PKG-INFO +5 -2
  6. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx.egg-info/SOURCES.txt +1 -0
  7. scholarx-2.3.4/tests/test_operation_tools.py +109 -0
  8. {scholarx-2.3.3 → scholarx-2.3.4}/LICENSE +0 -0
  9. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/__init__.py +0 -0
  10. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/__main__.py +0 -0
  11. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/api_client.py +0 -0
  12. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/cli.py +0 -0
  13. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/connectors/__init__.py +0 -0
  14. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/connectors/mcp_source_presets.json +0 -0
  15. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/connectors/tool_schema_fingerprints.json +0 -0
  16. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/deduplication.py +0 -0
  17. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/kg_ingest.py +0 -0
  18. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/kg_media.py +0 -0
  19. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/mcp_config.json +0 -0
  20. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/models.py +0 -0
  21. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/__init__.py +0 -0
  22. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/certification.json +0 -0
  23. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/fixtures/records.json +0 -0
  24. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/mappings/source.yaml +0 -0
  25. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/migrations/manifest.json +0 -0
  26. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/scholarx.ttl +0 -0
  27. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/ontology/shapes/connector.shacl.ttl +0 -0
  28. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/paper_storage.py +0 -0
  29. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/prompts/__init__.py +0 -0
  30. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/__init__.py +0 -0
  31. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/arxiv.py +0 -0
  32. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/base.py +0 -0
  33. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/biorxiv.py +0 -0
  34. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/osf.py +0 -0
  35. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/pmc.py +0 -0
  36. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/rss.py +0 -0
  37. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/providers/semantic_scholar.py +0 -0
  38. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/queue.py +0 -0
  39. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/__init__.py +0 -0
  40. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-kg-ingestion/SKILL.md +0 -0
  41. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-kg-ingestion/WORKFLOW.md +0 -0
  42. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-operations/SKILL.md +0 -0
  43. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-operations/agents/openai.yaml +0 -0
  44. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-operations/references/catalog.md +0 -0
  45. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-library/SKILL.md +0 -0
  46. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-library/WORKFLOW.md +0 -0
  47. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-search/SKILL.md +0 -0
  48. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-search/WORKFLOW.md +0 -0
  49. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx.egg-info/dependency_links.txt +0 -0
  50. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx.egg-info/entry_points.txt +0 -0
  51. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx.egg-info/requires.txt +0 -0
  52. {scholarx-2.3.3 → scholarx-2.3.4}/scholarx.egg-info/top_level.txt +0 -0
  53. {scholarx-2.3.3 → scholarx-2.3.4}/setup.cfg +0 -0
  54. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_action_discovery.py +0 -0
  55. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_cli.py +0 -0
  56. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_cli_extended.py +0 -0
  57. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_default_content_surfaces.py +0 -0
  58. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_init_dynamics.py +0 -0
  59. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_kg_ingest.py +0 -0
  60. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_kg_media.py +0 -0
  61. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_mcp_server.py +0 -0
  62. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_models.py +0 -0
  63. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_paper_storage.py +0 -0
  64. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_parallel_download.py +0 -0
  65. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_providers.py +0 -0
  66. {scholarx-2.3.3 → scholarx-2.3.4}/tests/test_queue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scholarx
3
- Version: 2.3.3
3
+ Version: 2.3.4
4
4
  Summary: Universal Research Paper API — single entry point for arXiv, PMC, bioRxiv, medRxiv, PsyArXiv, OSF, and Semantic Scholar
5
5
  Author-email: Project Maintainers <maintainers@users.noreply.github.com>
6
6
  License: MIT
@@ -48,7 +48,7 @@ Dynamic: license-file
48
48
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/scholarx)
49
49
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/scholarx)
50
50
 
51
- *Version: 2.3.3*
51
+ *Version: 2.3.4*
52
52
 
53
53
  > **Documentation** — Installation, deployment, usage across the API, CLI, MCP, and
54
54
  > agent interfaces are maintained in the
@@ -96,6 +96,9 @@ The table below is auto-generated from the MCP server — do not edit by hand.
96
96
  | `sx_info` | `DISCOVERYTOOL` | Get metadata about sources and categories. |
97
97
  | `sx_search` | `SEARCHTOOL` | Search for research papers across all configured sources. |
98
98
  | `sx_storage` | `STORAGETOOL` | Manage offline PDF storage and background downloads. |
99
+ | `sx_enabled_sources` | `OPERATIONTOOL` | List the paper sources enabled on this server (read-only). |
100
+ | `sx_download_papers` | `OPERATIONTOOL` | Download many papers in parallel. |
101
+ | `sx_download_urls` | `OPERATIONTOOL` | Download arXiv PDFs directly by id or URL in parallel. |
99
102
 
100
103
  #### Verbose 1:1 API-mapped tools (`MCP_TOOL_MODE=verbose` or `both`)
101
104
 
@@ -20,7 +20,7 @@
20
20
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/scholarx)
21
21
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/scholarx)
22
22
 
23
- *Version: 2.3.3*
23
+ *Version: 2.3.4*
24
24
 
25
25
  > **Documentation** — Installation, deployment, usage across the API, CLI, MCP, and
26
26
  > agent interfaces are maintained in the
@@ -68,6 +68,9 @@ The table below is auto-generated from the MCP server — do not edit by hand.
68
68
  | `sx_info` | `DISCOVERYTOOL` | Get metadata about sources and categories. |
69
69
  | `sx_search` | `SEARCHTOOL` | Search for research papers across all configured sources. |
70
70
  | `sx_storage` | `STORAGETOOL` | Manage offline PDF storage and background downloads. |
71
+ | `sx_enabled_sources` | `OPERATIONTOOL` | List the paper sources enabled on this server (read-only). |
72
+ | `sx_download_papers` | `OPERATIONTOOL` | Download many papers in parallel. |
73
+ | `sx_download_urls` | `OPERATIONTOOL` | Download arXiv PDFs directly by id or URL in parallel. |
71
74
 
72
75
  #### Verbose 1:1 API-mapped tools (`MCP_TOOL_MODE=verbose` or `both`)
73
76
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scholarx"
7
- version = "2.3.3"
7
+ version = "2.3.4"
8
8
  description = "Universal Research Paper API — single entry point for arXiv, PMC, bioRxiv, medRxiv, PsyArXiv, OSF, and Semantic Scholar"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -19,7 +19,7 @@ from pydantic import Field
19
19
 
20
20
  load_config()
21
21
 
22
- __version__ = "2.3.3"
22
+ __version__ = "2.3.4"
23
23
 
24
24
  # Wall budget for an inline single-paper download before it is handed to the
25
25
  # background queue. Kept well under the MCP child-call timeout so a slow source
@@ -501,6 +501,66 @@ def register_storage_tools(mcp):
501
501
  return {"error": f"Unknown action: {action}"} # pragma: no cover
502
502
 
503
503
 
504
+ async def _sx_resolve_papers(client, paper_source, ids) -> list:
505
+ """Resolve each id to a Paper; ids that cannot be resolved are dropped."""
506
+ found = await asyncio.gather(*(client.get_paper(paper_source, pid) for pid in ids))
507
+ return [paper for paper in found if paper]
508
+
509
+
510
+ def register_operation_tools(mcp):
511
+ """Register one tool per API-client operation not covered by the routed tools."""
512
+
513
+ @mcp.tool(tags={"discovery"}, annotations={"readOnlyHint": True})
514
+ async def sx_enabled_sources() -> dict:
515
+ """List the paper sources enabled on this server (``ScholarXClient.enabled_sources``)."""
516
+ client = _get_client()
517
+ return {"sources": [s.value for s in client.enabled_sources]}
518
+
519
+ @mcp.tool(
520
+ tags={"storage"},
521
+ annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": True},
522
+ )
523
+ async def sx_download_papers(
524
+ source: str = Field(description="Paper source (arxiv, pmc, etc.)"),
525
+ paper_ids: str = Field(description="Comma-separated paper IDs to download in parallel"),
526
+ concurrency: int = Field(default=0, description="Max simultaneous downloads; 0 auto-sizes"),
527
+ ) -> dict:
528
+ """Download many papers in parallel (``ScholarXClient.download_papers``)."""
529
+ from scholarx.models import PaperSource
530
+
531
+ error, paper_source, ids = _validate_bulk_download_request(source, paper_ids, PaperSource)
532
+ if error:
533
+ return error
534
+ client = _get_client()
535
+ papers = await _sx_resolve_papers(client, paper_source, ids)
536
+ try:
537
+ results = await client.download_papers(papers, concurrency=concurrency)
538
+ except ValueError:
539
+ return {"error": "Invalid download request"}
540
+ return {"results": results}
541
+
542
+ @mcp.tool(
543
+ tags={"storage"},
544
+ annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": True},
545
+ )
546
+ async def sx_download_urls(
547
+ paper_ids: str = Field(description="Comma-separated arXiv ids or abs/pdf URLs"),
548
+ concurrency: int = Field(default=0, description="Max simultaneous downloads; 0 auto-sizes"),
549
+ ) -> dict:
550
+ """Download arXiv PDFs directly by id or URL in parallel (``ScholarXClient.download_urls``)."""
551
+ if not paper_ids:
552
+ return {"error": "'paper_ids' required"}
553
+ if len(paper_ids) > _MAX_DIRECT_DOWNLOAD_INPUT_CHARS:
554
+ return {"error": "'paper_ids' input limit exceeded"}
555
+ raw_ids = [i.strip() for i in paper_ids.split(",") if i.strip()]
556
+ client = _get_client()
557
+ try:
558
+ results = await client.download_urls(raw_ids, concurrency=concurrency)
559
+ except ValueError:
560
+ return {"error": "Invalid download request"}
561
+ return {"results": results}
562
+
563
+
504
564
  def register_kg_tools(mcp):
505
565
  """Register native knowledge-graph ingestion tools (Wire-First)."""
506
566
 
@@ -600,6 +660,7 @@ def get_mcp_instance():
600
660
  register_search_tools,
601
661
  register_discovery_tools,
602
662
  register_storage_tools,
663
+ register_operation_tools,
603
664
  register_kg_tools,
604
665
  ],
605
666
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scholarx
3
- Version: 2.3.3
3
+ Version: 2.3.4
4
4
  Summary: Universal Research Paper API — single entry point for arXiv, PMC, bioRxiv, medRxiv, PsyArXiv, OSF, and Semantic Scholar
5
5
  Author-email: Project Maintainers <maintainers@users.noreply.github.com>
6
6
  License: MIT
@@ -48,7 +48,7 @@ Dynamic: license-file
48
48
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/scholarx)
49
49
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/scholarx)
50
50
 
51
- *Version: 2.3.3*
51
+ *Version: 2.3.4*
52
52
 
53
53
  > **Documentation** — Installation, deployment, usage across the API, CLI, MCP, and
54
54
  > agent interfaces are maintained in the
@@ -96,6 +96,9 @@ The table below is auto-generated from the MCP server — do not edit by hand.
96
96
  | `sx_info` | `DISCOVERYTOOL` | Get metadata about sources and categories. |
97
97
  | `sx_search` | `SEARCHTOOL` | Search for research papers across all configured sources. |
98
98
  | `sx_storage` | `STORAGETOOL` | Manage offline PDF storage and background downloads. |
99
+ | `sx_enabled_sources` | `OPERATIONTOOL` | List the paper sources enabled on this server (read-only). |
100
+ | `sx_download_papers` | `OPERATIONTOOL` | Download many papers in parallel. |
101
+ | `sx_download_urls` | `OPERATIONTOOL` | Download arXiv PDFs directly by id or URL in parallel. |
99
102
 
100
103
  #### Verbose 1:1 API-mapped tools (`MCP_TOOL_MODE=verbose` or `both`)
101
104
 
@@ -57,6 +57,7 @@ tests/test_kg_ingest.py
57
57
  tests/test_kg_media.py
58
58
  tests/test_mcp_server.py
59
59
  tests/test_models.py
60
+ tests/test_operation_tools.py
60
61
  tests/test_paper_storage.py
61
62
  tests/test_parallel_download.py
62
63
  tests/test_providers.py
@@ -0,0 +1,109 @@
1
+ """CONN-SCHOLARX-API-COVERAGE-R001: one MCP tool per remaining API operation, coverage at the 90 baseline."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+
8
+ import pytest
9
+
10
+ from scholarx.mcp_server import register_operation_tools
11
+ from scholarx.models import PaperSource
12
+
13
+ pytestmark = pytest.mark.spec("CONN-SCHOLARX-API-COVERAGE-R001")
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+
17
+
18
+ class MockMCP:
19
+ def __init__(self):
20
+ self.tools = {}
21
+ self.kwargs = {}
22
+
23
+ def tool(self, *args, **kwargs):
24
+ def decorator(func):
25
+ self.tools[func.__name__] = func
26
+ self.kwargs[func.__name__] = kwargs
27
+ return func
28
+
29
+ return decorator
30
+
31
+
32
+ @pytest.fixture
33
+ def tools():
34
+ mcp = MockMCP()
35
+ register_operation_tools(mcp)
36
+ return mcp
37
+
38
+
39
+ def test_tools_are_registered_with_hints(tools):
40
+ assert set(tools.tools) == {"sx_enabled_sources", "sx_download_papers", "sx_download_urls"}
41
+ assert tools.kwargs["sx_enabled_sources"]["annotations"]["readOnlyHint"] is True
42
+ assert tools.kwargs["sx_download_papers"]["annotations"]["readOnlyHint"] is False
43
+ assert tools.kwargs["sx_download_urls"]["annotations"]["readOnlyHint"] is False
44
+
45
+
46
+ @pytest.mark.asyncio
47
+ async def test_enabled_sources(tools):
48
+ client = MagicMock(enabled_sources=[PaperSource.ARXIV])
49
+ with patch("scholarx.mcp_server._get_client", return_value=client):
50
+ assert await tools.tools["sx_enabled_sources"]() == {"sources": ["arxiv"]}
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_download_papers(tools):
55
+ client = MagicMock()
56
+ client.get_paper = AsyncMock(side_effect=lambda src, pid: MagicMock(id=pid))
57
+ client.download_papers = AsyncMock(return_value=[{"paper_id": "1", "status": "downloaded"}])
58
+ with patch("scholarx.mcp_server._get_client", return_value=client):
59
+ out = await tools.tools["sx_download_papers"](source="arxiv", paper_ids="1,2", concurrency=3)
60
+ assert out == {"results": [{"paper_id": "1", "status": "downloaded"}]}
61
+ assert len(client.download_papers.call_args.args[0]) == 2
62
+ assert client.download_papers.call_args.kwargs == {"concurrency": 3}
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_download_papers_rejects_bad_input(tools):
67
+ with patch("scholarx.mcp_server._get_client", return_value=MagicMock()):
68
+ out = await tools.tools["sx_download_papers"](source="nope", paper_ids="1", concurrency=0)
69
+ assert "error" in out
70
+
71
+
72
+ @pytest.mark.asyncio
73
+ async def test_download_urls(tools):
74
+ client = MagicMock()
75
+ client.download_urls = AsyncMock(return_value=[{"paper_id": "2603.09022", "status": "cached"}])
76
+ with patch("scholarx.mcp_server._get_client", return_value=client):
77
+ out = await tools.tools["sx_download_urls"](paper_ids="2603.09022, 2603.09023", concurrency=0)
78
+ assert out["results"][0]["status"] == "cached"
79
+ assert client.download_urls.call_args.args[0] == ["2603.09022", "2603.09023"]
80
+
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_download_urls_requires_ids_and_maps_limit_errors(tools):
84
+ client = MagicMock()
85
+ client.download_urls = AsyncMock(side_effect=ValueError("limit"))
86
+ with patch("scholarx.mcp_server._get_client", return_value=client):
87
+ assert "error" in await tools.tools["sx_download_urls"](paper_ids="", concurrency=0)
88
+ assert "error" in await tools.tools["sx_download_urls"](paper_ids="1", concurrency=0)
89
+
90
+
91
+ def test_coverage_gate_holds_at_the_baseline():
92
+ script = (ROOT / "scripts/verify_api_integration.py").read_text()
93
+ assert '"scholarx": 90.0' in script
94
+ # The checker names the agent from the directory; run it from a directory called scholarx.
95
+ result = subprocess.run(
96
+ [
97
+ sys.executable,
98
+ "-c",
99
+ "import sys, importlib.util as u;"
100
+ "s=u.spec_from_file_location('v', sys.argv[1]);m=u.module_from_spec(s);s.loader.exec_module(m);"
101
+ "r=m.verify_agent(sys.argv[2]);print(r['coverage'])",
102
+ str(ROOT / "scripts/verify_api_integration.py"),
103
+ str(ROOT),
104
+ ],
105
+ capture_output=True,
106
+ text=True,
107
+ check=True,
108
+ )
109
+ assert float(result.stdout.strip()) >= 90.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes