scholarx 2.3.2__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.
- {scholarx-2.3.2/scholarx.egg-info → scholarx-2.3.4}/PKG-INFO +7 -4
- {scholarx-2.3.2 → scholarx-2.3.4}/README.md +4 -1
- {scholarx-2.3.2 → scholarx-2.3.4}/pyproject.toml +7 -3
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/connectors/tool_schema_fingerprints.json +1 -1
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/kg_ingest.py +4 -6
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/kg_media.py +1 -1
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/mcp_server.py +62 -1
- {scholarx-2.3.2 → scholarx-2.3.4/scholarx.egg-info}/PKG-INFO +7 -4
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx.egg-info/SOURCES.txt +2 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx.egg-info/entry_points.txt +3 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx.egg-info/requires.txt +2 -2
- scholarx-2.3.4/tests/test_default_content_surfaces.py +43 -0
- scholarx-2.3.4/tests/test_operation_tools.py +109 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/LICENSE +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/__init__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/__main__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/api_client.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/cli.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/connectors/__init__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/connectors/mcp_source_presets.json +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/deduplication.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/mcp_config.json +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/models.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/__init__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/certification.json +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/fixtures/records.json +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/mappings/source.yaml +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/migrations/manifest.json +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/scholarx.ttl +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/ontology/shapes/connector.shacl.ttl +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/paper_storage.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/prompts/__init__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/__init__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/arxiv.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/base.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/biorxiv.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/osf.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/pmc.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/rss.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/providers/semantic_scholar.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/queue.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/__init__.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-kg-ingestion/SKILL.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-kg-ingestion/WORKFLOW.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-operations/SKILL.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-operations/agents/openai.yaml +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-operations/references/catalog.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-library/SKILL.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-library/WORKFLOW.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-search/SKILL.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx/skills/scholarx-paper-search/WORKFLOW.md +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx.egg-info/dependency_links.txt +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/scholarx.egg-info/top_level.txt +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/setup.cfg +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_action_discovery.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_cli.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_cli_extended.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_init_dynamics.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_kg_ingest.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_kg_media.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_mcp_server.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_models.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_paper_storage.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_parallel_download.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_providers.py +0 -0
- {scholarx-2.3.2 → scholarx-2.3.4}/tests/test_queue.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scholarx
|
|
3
|
-
Version: 2.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
|
|
7
7
|
Requires-Python: <3.15,>=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.
|
|
10
|
+
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.18
|
|
11
11
|
Requires-Dist: httpx>=0.28.1
|
|
12
12
|
Requires-Dist: python-dotenv>=1.2.4
|
|
13
13
|
Requires-Dist: Levenshtein>=0.27.5
|
|
@@ -15,7 +15,7 @@ Requires-Dist: pypdf>=6.21.0
|
|
|
15
15
|
Requires-Dist: rich>=15.0.0
|
|
16
16
|
Requires-Dist: defusedxml>=0.7.1
|
|
17
17
|
Provides-Extra: mcp
|
|
18
|
-
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.
|
|
18
|
+
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.18; extra == "mcp"
|
|
19
19
|
Provides-Extra: all
|
|
20
20
|
Requires-Dist: scholarx[mcp]; extra == "all"
|
|
21
21
|
Provides-Extra: test
|
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
|
48
48
|

|
|
49
49
|

|
|
50
50
|
|
|
51
|
-
*Version: 2.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
|

|
|
21
21
|

|
|
22
22
|
|
|
23
|
-
*Version: 2.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,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scholarx"
|
|
7
|
-
version = "2.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" }
|
|
11
11
|
requires-python = ">=3.12, <3.15"
|
|
12
12
|
authors = [{ name = "Project Maintainers", email = "maintainers@users.noreply.github.com" }]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"agent-connector-sdk[mcp]>=0.3.
|
|
14
|
+
"agent-connector-sdk[mcp]>=0.3.18,<1.0.0",
|
|
15
15
|
"httpx>=0.28.1",
|
|
16
16
|
"python-dotenv>=1.2.4",
|
|
17
17
|
"Levenshtein>=0.27.5",
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.optional-dependencies]
|
|
24
|
-
mcp = ["agent-connector-sdk[mcp]>=0.3.
|
|
24
|
+
mcp = ["agent-connector-sdk[mcp]>=0.3.18,<1.0.0"]
|
|
25
25
|
all = ["scholarx[mcp]"]
|
|
26
26
|
test = [
|
|
27
27
|
"pytest-xdist>=3.8.0","pytest>=9.1.1", "pytest-asyncio>=1.4.0", "pytest-cov>=7.1.0", "respx>=0.23.1"]
|
|
@@ -39,6 +39,10 @@ scholarx = "scholarx.ontology"
|
|
|
39
39
|
[project.entry-points."agent_connector_sdk.source_connector_providers"]
|
|
40
40
|
scholarx = "scholarx.connectors"
|
|
41
41
|
|
|
42
|
+
# The one declared connector identity: the fleet name (SDK-CONNECTOR-CONTROL-R071).
|
|
43
|
+
[project.entry-points."agent_connector_sdk.connector"]
|
|
44
|
+
scholarx-mcp = "scholarx"
|
|
45
|
+
|
|
42
46
|
[tool.uv.workspace]
|
|
43
47
|
members = ["."]
|
|
44
48
|
|
|
@@ -33,13 +33,13 @@ from agent_connector_sdk.ingest import (
|
|
|
33
33
|
|
|
34
34
|
logger = logging.getLogger("scholarx.kg")
|
|
35
35
|
|
|
36
|
-
_BINDING = IngestBinding(connector="scholarx", stream="scholarx")
|
|
36
|
+
_BINDING = IngestBinding(connector="scholarx-mcp", stream="scholarx")
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def _to_entity(record: dict[str, Any]) -> Entity:
|
|
40
40
|
return Entity(
|
|
41
|
-
id=record.get("id"),
|
|
42
|
-
node_type=record.get("node_type"),
|
|
41
|
+
id=str(record.get("id") or ""),
|
|
42
|
+
node_type=str(record.get("node_type") or ""),
|
|
43
43
|
properties={k: v for k, v in record.items() if k not in ("id", "node_type")},
|
|
44
44
|
)
|
|
45
45
|
|
|
@@ -60,9 +60,7 @@ def _to_document(record: dict[str, Any]) -> Document:
|
|
|
60
60
|
text=record["text"],
|
|
61
61
|
title=record.get("title"),
|
|
62
62
|
source_uri=record.get("source_uri"),
|
|
63
|
-
properties={
|
|
64
|
-
k: v for k, v in record.items() if k not in ("id", "text", "title", "source_uri", "updated_at")
|
|
65
|
-
},
|
|
63
|
+
properties={k: v for k, v in record.items() if k not in ("id", "text", "title", "source_uri", "updated_at")},
|
|
66
64
|
updated_at=record.get("updated_at"),
|
|
67
65
|
)
|
|
68
66
|
|
|
@@ -31,7 +31,7 @@ from agent_connector_sdk.ingest import (
|
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger("scholarx.kg")
|
|
33
33
|
|
|
34
|
-
_BINDING = IngestBinding(connector="scholarx", stream="scholarx")
|
|
34
|
+
_BINDING = IngestBinding(connector="scholarx-mcp", stream="scholarx")
|
|
35
35
|
|
|
36
36
|
# Paper metadata worth carrying onto the stored media asset's properties.
|
|
37
37
|
_META_FIELDS = ("id", "title", "doi", "url", "source", "published_date")
|
|
@@ -19,7 +19,7 @@ from pydantic import Field
|
|
|
19
19
|
|
|
20
20
|
load_config()
|
|
21
21
|
|
|
22
|
-
__version__ = "2.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,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scholarx
|
|
3
|
-
Version: 2.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
|
|
7
7
|
Requires-Python: <3.15,>=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.
|
|
10
|
+
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.18
|
|
11
11
|
Requires-Dist: httpx>=0.28.1
|
|
12
12
|
Requires-Dist: python-dotenv>=1.2.4
|
|
13
13
|
Requires-Dist: Levenshtein>=0.27.5
|
|
@@ -15,7 +15,7 @@ Requires-Dist: pypdf>=6.21.0
|
|
|
15
15
|
Requires-Dist: rich>=15.0.0
|
|
16
16
|
Requires-Dist: defusedxml>=0.7.1
|
|
17
17
|
Provides-Extra: mcp
|
|
18
|
-
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.
|
|
18
|
+
Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.18; extra == "mcp"
|
|
19
19
|
Provides-Extra: all
|
|
20
20
|
Requires-Dist: scholarx[mcp]; extra == "all"
|
|
21
21
|
Provides-Extra: test
|
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
|
48
48
|

|
|
49
49
|

|
|
50
50
|
|
|
51
|
-
*Version: 2.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
|
|
|
@@ -51,11 +51,13 @@ scholarx/skills/scholarx-paper-search/WORKFLOW.md
|
|
|
51
51
|
tests/test_action_discovery.py
|
|
52
52
|
tests/test_cli.py
|
|
53
53
|
tests/test_cli_extended.py
|
|
54
|
+
tests/test_default_content_surfaces.py
|
|
54
55
|
tests/test_init_dynamics.py
|
|
55
56
|
tests/test_kg_ingest.py
|
|
56
57
|
tests/test_kg_media.py
|
|
57
58
|
tests/test_mcp_server.py
|
|
58
59
|
tests/test_models.py
|
|
60
|
+
tests/test_operation_tools.py
|
|
59
61
|
tests/test_paper_storage.py
|
|
60
62
|
tests/test_parallel_download.py
|
|
61
63
|
tests/test_providers.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
agent-connector-sdk[mcp]<1.0.0,>=0.3.
|
|
1
|
+
agent-connector-sdk[mcp]<1.0.0,>=0.3.18
|
|
2
2
|
httpx>=0.28.1
|
|
3
3
|
python-dotenv>=1.2.4
|
|
4
4
|
Levenshtein>=0.27.5
|
|
@@ -10,7 +10,7 @@ defusedxml>=0.7.1
|
|
|
10
10
|
scholarx[mcp]
|
|
11
11
|
|
|
12
12
|
[mcp]
|
|
13
|
-
agent-connector-sdk[mcp]<1.0.0,>=0.3.
|
|
13
|
+
agent-connector-sdk[mcp]<1.0.0,>=0.3.18
|
|
14
14
|
|
|
15
15
|
[test]
|
|
16
16
|
pytest-xdist>=3.8.0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""CONN-SCHOLARX-CONNECTOR-CONTENT-R001: the server lists its packaged skills, ontology and prompts by default."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from fastmcp import Client
|
|
11
|
+
|
|
12
|
+
PACKAGE = "scholarx"
|
|
13
|
+
ROOT = Path(list(importlib.import_module(PACKAGE).__path__)[0])
|
|
14
|
+
|
|
15
|
+
pytestmark = pytest.mark.spec("CONN-SCHOLARX-CONNECTOR-CONTENT-R001")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _server():
|
|
19
|
+
namespace = {"__name__": PACKAGE + ".mcp_server"}
|
|
20
|
+
exec(
|
|
21
|
+
"from agent_connector_sdk.mcp.server import create_mcp_server\n"
|
|
22
|
+
"result = create_mcp_server('scholarx-mcp', version='0', command_args=[])",
|
|
23
|
+
namespace,
|
|
24
|
+
)
|
|
25
|
+
return namespace["result"][1]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _listings():
|
|
29
|
+
async with Client(_server()) as client:
|
|
30
|
+
resources = {str(r.uri) for r in await client.list_resources()}
|
|
31
|
+
prompts = {p.name for p in await client.list_prompts()}
|
|
32
|
+
return resources, prompts
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_default_content_surfaces() -> None:
|
|
36
|
+
resources, prompts = asyncio.run(_listings())
|
|
37
|
+
if list(ROOT.glob("skills/*/SKILL.md")):
|
|
38
|
+
assert any(u.startswith("skill://") for u in resources)
|
|
39
|
+
if list(ROOT.glob("ontology/*.ttl")):
|
|
40
|
+
assert any(u.startswith("ontology://scholarx-mcp/") for u in resources)
|
|
41
|
+
for path in ROOT.glob("prompts/*.json"):
|
|
42
|
+
if not path.name.startswith("_"):
|
|
43
|
+
assert path.stem in prompts
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|