uniarticles-mcp 0.1.0__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.
@@ -0,0 +1,2 @@
1
+ SCOPUS_API_KEY=your_scopus_api_key_here
2
+ SEMANTIC_SCHOLAR_API_KEY=your_semantic_scholar_api_key_here
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: uniarticles-mcp
3
+ Version: 0.1.0
4
+ Summary: Unified MCP server for multi-source academic literature retrieval
5
+ Project-URL: Homepage, https://github.com/yourusername/uniarticles-mcp
6
+ Project-URL: Repository, https://github.com/yourusername/uniarticles-mcp
7
+ Project-URL: Issues, https://github.com/yourusername/uniarticles-mcp/issues
8
+ Author-email: Your Name <your.email@example.com>
9
+ License: MIT
10
+ Keywords: academic,arxiv,chemrxiv,mcp,research,scopus,semanticscholar
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: arxiv>=2.1.0
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: mcp>=1.0.0
22
+ Requires-Dist: python-dotenv>=1.0.0
23
+ Requires-Dist: semanticscholar>=0.8.4
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # UniArticles MCP Server
29
+
30
+ A unified Model Context Protocol (MCP) server for retrieving academic literature from multiple sources.
31
+
32
+ ## 🌟 Features
33
+
34
+ - **Unified Interface**: Consistent return structure across all data sources.
35
+ - **Multi-Source Support**:
36
+ - **Scopus**: Extensive abstract and citation database.
37
+ - **Semantic Scholar**: AI-driven research tool.
38
+ - **ArXiv**: Open access archive for scholarly articles.
39
+ - **ChemRxiv**: Open access preprint archive for chemistry.
40
+ - **MCP Standard**: Built on the FastMCP framework for easy integration with Claude and other MCP clients.
41
+
42
+ ## 🚀 Installation
43
+
44
+ ### Prerequisites
45
+ - Python 3.10 or higher
46
+ - `uv` (recommended) or `pip`
47
+
48
+ ### Using uv (Recommended)
49
+
50
+ ```bash
51
+ # Clone the repository
52
+ git clone <repository-url>
53
+ cd UniArticles_MCPserver
54
+
55
+ # Install dependencies
56
+ uv sync
57
+ ```
58
+
59
+ ### Using pip
60
+
61
+ ```bash
62
+ pip install -e .
63
+ ```
64
+
65
+ ## ⚙️ Configuration
66
+
67
+ Create a `.env` file in the project root or set environment variables:
68
+
69
+ ```env
70
+ # Required for Scopus
71
+ SCOPUS_API_KEY=your_scopus_api_key
72
+
73
+ # Optional (Recommended for higher limits)
74
+ SEMANTICSCHOLAR_API_KEY=your_semanticscholar_api_key
75
+ ```
76
+
77
+ ### ⚠️ Important Notes
78
+ - **API Key Handling**: If you do not have an API key for a service (e.g., Semantic Scholar), **you must delete the corresponding line** from the configuration. Do not leave it as `your_key_here`, otherwise the service will try to authenticate with that invalid string and fail (e.g., returning 403 Forbidden).
79
+ - **Scopus**: If `SCOPUS_API_KEY` is missing, Scopus search will return errors, but other tools will work.
80
+ - **Semantic Scholar**: If `SEMANTICSCHOLAR_API_KEY` is missing (i.e., the line is deleted), the service will automatically fallback to the public API (lower rate limits but functional).
81
+
82
+ ## 🏃 Usage
83
+
84
+ ### Running Locally
85
+
86
+ ```bash
87
+ # Using uv
88
+ uv run uniarticles-mcp
89
+
90
+ # Using python directly
91
+ python -m uniarticles
92
+ ```
93
+
94
+ ### With Claude Desktop
95
+
96
+ #### Option 1: Via PyPI (Recommended if published)
97
+ If you have published the package to PyPI (or are installing from a git URL), use `uvx`:
98
+
99
+ > **Note**: In the example below, `SEMANTICSCHOLAR_API_KEY` is omitted. Remove it if you don't have a key.
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "uniarticles": {
105
+ "command": "uvx",
106
+ "args": [
107
+ "uniarticles-mcp"
108
+ ],
109
+ "env": {
110
+ "SCOPUS_API_KEY": "your_scopus_key_here"
111
+ }
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ #### Option 2: Full Configuration (With all keys)
118
+ ```json
119
+ {
120
+ "mcpServers": {
121
+ "uniarticles": {
122
+ "command": "uvx",
123
+ "args": [
124
+ "uniarticles-mcp"
125
+ ],
126
+ "env": {
127
+ "SCOPUS_API_KEY": "your_scopus_key_here",
128
+ "SEMANTICSCHOLAR_API_KEY": "your_semanticscholar_key_here"
129
+ }
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ #### Option 3: Local Development
136
+ If you are running from source code locally:
137
+
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "uniarticles": {
142
+ "command": "uv",
143
+ "args": [
144
+ "--directory",
145
+ "C:/absolute/path/to/UniArticles_MCPserver/UniArticles_MCPserver",
146
+ "run",
147
+ "uniarticles-mcp"
148
+ ],
149
+ "env": {
150
+ "SCOPUS_API_KEY": "your_scopus_key_here",
151
+ "SEMANTICSCHOLAR_API_KEY": "your_semanticscholar_key_here"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ## 🛠️ Development
159
+
160
+ ### Running Tests
161
+
162
+ ```bash
163
+ # Run integration tests
164
+ python -m unittest tests/test_integration.py
165
+ ```
166
+
167
+ ### Project Structure
168
+
169
+ ```text
170
+ src/uniarticles/
171
+ ├── sources/ # Individual data source modules
172
+ │ ├── arxiv.py
173
+ │ ├── scopus.py
174
+ │ ├── ...
175
+ ├── server.py # Main server entry point
176
+ └── config.py # Configuration management
177
+ ```
178
+
179
+ ## 📄 License
180
+
181
+ [MIT License](LICENSE)
@@ -0,0 +1,154 @@
1
+ # UniArticles MCP Server
2
+
3
+ A unified Model Context Protocol (MCP) server for retrieving academic literature from multiple sources.
4
+
5
+ ## 🌟 Features
6
+
7
+ - **Unified Interface**: Consistent return structure across all data sources.
8
+ - **Multi-Source Support**:
9
+ - **Scopus**: Extensive abstract and citation database.
10
+ - **Semantic Scholar**: AI-driven research tool.
11
+ - **ArXiv**: Open access archive for scholarly articles.
12
+ - **ChemRxiv**: Open access preprint archive for chemistry.
13
+ - **MCP Standard**: Built on the FastMCP framework for easy integration with Claude and other MCP clients.
14
+
15
+ ## 🚀 Installation
16
+
17
+ ### Prerequisites
18
+ - Python 3.10 or higher
19
+ - `uv` (recommended) or `pip`
20
+
21
+ ### Using uv (Recommended)
22
+
23
+ ```bash
24
+ # Clone the repository
25
+ git clone <repository-url>
26
+ cd UniArticles_MCPserver
27
+
28
+ # Install dependencies
29
+ uv sync
30
+ ```
31
+
32
+ ### Using pip
33
+
34
+ ```bash
35
+ pip install -e .
36
+ ```
37
+
38
+ ## ⚙️ Configuration
39
+
40
+ Create a `.env` file in the project root or set environment variables:
41
+
42
+ ```env
43
+ # Required for Scopus
44
+ SCOPUS_API_KEY=your_scopus_api_key
45
+
46
+ # Optional (Recommended for higher limits)
47
+ SEMANTICSCHOLAR_API_KEY=your_semanticscholar_api_key
48
+ ```
49
+
50
+ ### ⚠️ Important Notes
51
+ - **API Key Handling**: If you do not have an API key for a service (e.g., Semantic Scholar), **you must delete the corresponding line** from the configuration. Do not leave it as `your_key_here`, otherwise the service will try to authenticate with that invalid string and fail (e.g., returning 403 Forbidden).
52
+ - **Scopus**: If `SCOPUS_API_KEY` is missing, Scopus search will return errors, but other tools will work.
53
+ - **Semantic Scholar**: If `SEMANTICSCHOLAR_API_KEY` is missing (i.e., the line is deleted), the service will automatically fallback to the public API (lower rate limits but functional).
54
+
55
+ ## 🏃 Usage
56
+
57
+ ### Running Locally
58
+
59
+ ```bash
60
+ # Using uv
61
+ uv run uniarticles-mcp
62
+
63
+ # Using python directly
64
+ python -m uniarticles
65
+ ```
66
+
67
+ ### With Claude Desktop
68
+
69
+ #### Option 1: Via PyPI (Recommended if published)
70
+ If you have published the package to PyPI (or are installing from a git URL), use `uvx`:
71
+
72
+ > **Note**: In the example below, `SEMANTICSCHOLAR_API_KEY` is omitted. Remove it if you don't have a key.
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "uniarticles": {
78
+ "command": "uvx",
79
+ "args": [
80
+ "uniarticles-mcp"
81
+ ],
82
+ "env": {
83
+ "SCOPUS_API_KEY": "your_scopus_key_here"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ #### Option 2: Full Configuration (With all keys)
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "uniarticles": {
95
+ "command": "uvx",
96
+ "args": [
97
+ "uniarticles-mcp"
98
+ ],
99
+ "env": {
100
+ "SCOPUS_API_KEY": "your_scopus_key_here",
101
+ "SEMANTICSCHOLAR_API_KEY": "your_semanticscholar_key_here"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ #### Option 3: Local Development
109
+ If you are running from source code locally:
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "uniarticles": {
115
+ "command": "uv",
116
+ "args": [
117
+ "--directory",
118
+ "C:/absolute/path/to/UniArticles_MCPserver/UniArticles_MCPserver",
119
+ "run",
120
+ "uniarticles-mcp"
121
+ ],
122
+ "env": {
123
+ "SCOPUS_API_KEY": "your_scopus_key_here",
124
+ "SEMANTICSCHOLAR_API_KEY": "your_semanticscholar_key_here"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## 🛠️ Development
132
+
133
+ ### Running Tests
134
+
135
+ ```bash
136
+ # Run integration tests
137
+ python -m unittest tests/test_integration.py
138
+ ```
139
+
140
+ ### Project Structure
141
+
142
+ ```text
143
+ src/uniarticles/
144
+ ├── sources/ # Individual data source modules
145
+ │ ├── arxiv.py
146
+ │ ├── scopus.py
147
+ │ ├── ...
148
+ ├── server.py # Main server entry point
149
+ └── config.py # Configuration management
150
+ ```
151
+
152
+ ## 📄 License
153
+
154
+ [MIT License](LICENSE)
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "uniarticles-mcp-server": {
4
+ "command": "uvx",
5
+ "args": [
6
+ "uniarticles-mcp"
7
+ ],
8
+ "env": {
9
+ "SCOPUS_API_KEY": "4d32cadb485f8b9854988ffc06770cbc"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "uniarticles-mcp"
7
+ version = "0.1.0"
8
+ description = "Unified MCP server for multi-source academic literature retrieval"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Your Name", email = "your.email@example.com" }
13
+ ]
14
+ keywords = ["mcp", "academic", "research", "scopus", "arxiv", "semanticscholar", "chemrxiv"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Scientific/Engineering",
23
+ ]
24
+ requires-python = ">=3.10"
25
+ dependencies = [
26
+ "arxiv>=2.1.0",
27
+ "mcp>=1.0.0",
28
+ "httpx>=0.27.0",
29
+ "python-dotenv>=1.0.0",
30
+ "semanticscholar>=0.8.4",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/yourusername/uniarticles-mcp"
35
+ Repository = "https://github.com/yourusername/uniarticles-mcp"
36
+ Issues = "https://github.com/yourusername/uniarticles-mcp/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=8.0.0",
41
+ ]
42
+
43
+ [project.scripts]
44
+ uniarticles-mcp = "uniarticles.__main__:main"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/uniarticles"]
@@ -0,0 +1,3 @@
1
+ from .server import create_server
2
+
3
+ __all__ = ["create_server"]
@@ -0,0 +1,10 @@
1
+ from .server import create_server
2
+
3
+
4
+ def main() -> None:
5
+ server = create_server()
6
+ server.run()
7
+
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,16 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+ from dotenv import load_dotenv
5
+
6
+
7
+ load_dotenv()
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Settings:
12
+ scopus_api_key: str | None = os.getenv("SCOPUS_API_KEY")
13
+ semanticscholar_api_key: str | None = os.getenv("SEMANTICSCHOLAR_API_KEY")
14
+
15
+
16
+ settings = Settings()
@@ -0,0 +1,9 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+
3
+ from .sources import register_all_sources
4
+
5
+
6
+ def create_server() -> FastMCP:
7
+ server = FastMCP("uniarticles-mcp")
8
+ register_all_sources(server)
9
+ return server
@@ -0,0 +1,13 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+
3
+ from .arxiv import register as register_arxiv_source
4
+ from .chemrxiv import register as register_chemrxiv_source
5
+ from .scopus import register as register_scopus_source
6
+ from .semanticscholar import register as register_semanticscholar_source
7
+
8
+
9
+ def register_all_sources(server: FastMCP) -> None:
10
+ register_arxiv_source(server)
11
+ register_chemrxiv_source(server)
12
+ register_semanticscholar_source(server)
13
+ register_scopus_source(server)
@@ -0,0 +1,62 @@
1
+ import asyncio
2
+
3
+ import arxiv
4
+ from mcp.server.fastmcp import FastMCP
5
+
6
+
7
+ def _ok(query: str, items: list[dict]) -> dict:
8
+ return {
9
+ "ok": True,
10
+ "source": "arxiv",
11
+ "query": query,
12
+ "count": len(items),
13
+ "items": items,
14
+ "error": None,
15
+ }
16
+
17
+
18
+ def _err(query: str, message: str) -> dict:
19
+ return {
20
+ "ok": False,
21
+ "source": "arxiv",
22
+ "query": query,
23
+ "count": 0,
24
+ "items": [],
25
+ "error": message,
26
+ }
27
+
28
+
29
+ def _run_arxiv_search(query: str, max_results: int) -> dict:
30
+ client = arxiv.Client()
31
+ search = arxiv.Search(
32
+ query=query,
33
+ max_results=max_results,
34
+ sort_by=arxiv.SortCriterion.SubmittedDate,
35
+ )
36
+ papers = []
37
+ for paper in client.results(search):
38
+ papers.append(
39
+ {
40
+ "id": paper.get_short_id(),
41
+ "title": paper.title,
42
+ "authors": [author.name for author in paper.authors],
43
+ "abstract": paper.summary,
44
+ "published": paper.published.isoformat(),
45
+ "categories": paper.categories,
46
+ "pdf_url": paper.pdf_url,
47
+ }
48
+ )
49
+ return _ok(query=query, items=papers)
50
+
51
+
52
+ def register(server: FastMCP) -> None:
53
+ @server.tool()
54
+ async def search_arxiv(query: str, max_results: int = 10) -> dict:
55
+ normalized_query = query.strip()
56
+ bounded = max(1, min(max_results, 25))
57
+ if not normalized_query:
58
+ return _err(query=query, message="query must not be empty")
59
+ try:
60
+ return await asyncio.to_thread(_run_arxiv_search, normalized_query, bounded)
61
+ except Exception as exc:
62
+ return _err(query=normalized_query, message=str(exc))
@@ -0,0 +1,73 @@
1
+ import httpx
2
+ from mcp.server.fastmcp import FastMCP
3
+
4
+
5
+ BASE_URL = "https://chemrxiv.org/engage/chemrxiv/public-api/v1"
6
+
7
+
8
+ def _ok(term: str, items: list[dict]) -> dict:
9
+ return {
10
+ "ok": True,
11
+ "source": "chemrxiv",
12
+ "query": term,
13
+ "count": len(items),
14
+ "items": items,
15
+ "error": None,
16
+ }
17
+
18
+
19
+ def _err(term: str, message: str) -> dict:
20
+ return {
21
+ "ok": False,
22
+ "source": "chemrxiv",
23
+ "query": term,
24
+ "count": 0,
25
+ "items": [],
26
+ "error": message,
27
+ }
28
+
29
+
30
+ def _normalize_item(item: dict) -> dict:
31
+ return {
32
+ "itemId": item.get("itemId") or item.get("id"),
33
+ "doi": item.get("doi"),
34
+ "title": item.get("title"),
35
+ "abstract": item.get("abstract"),
36
+ "authors": item.get("authors"),
37
+ "publishedDate": item.get("publishedDate") or item.get("published"),
38
+ "url": item.get("url"),
39
+ "license": item.get("license"),
40
+ }
41
+
42
+
43
+ async def _search_items(term: str, limit: int, page: int | None, sort: str | None) -> dict:
44
+ params = {"term": term, "limit": limit}
45
+ if page is not None:
46
+ params["page"] = page
47
+ if sort:
48
+ params["sort"] = sort
49
+ async with httpx.AsyncClient(timeout=20.0) as client:
50
+ response = await client.get(f"{BASE_URL}/items", params=params)
51
+ response.raise_for_status()
52
+ payload = response.json()
53
+ items = payload.get("items") or payload.get("results") or []
54
+ normalized = [_normalize_item(item) for item in items]
55
+ return _ok(term=term, items=normalized)
56
+
57
+
58
+ def register(server: FastMCP) -> None:
59
+ @server.tool()
60
+ async def search_chemrxiv(
61
+ term: str,
62
+ limit: int = 10,
63
+ page: int | None = None,
64
+ sort: str | None = None,
65
+ ) -> dict:
66
+ normalized_term = term.strip()
67
+ bounded = max(1, min(limit, 50))
68
+ if not normalized_term:
69
+ return _err(term=term, message="term must not be empty")
70
+ try:
71
+ return await _search_items(term=normalized_term, limit=bounded, page=page, sort=sort)
72
+ except Exception as exc:
73
+ return _err(term=normalized_term, message=str(exc))
@@ -0,0 +1,77 @@
1
+ import httpx
2
+ from mcp.server.fastmcp import FastMCP
3
+
4
+ from ..config import settings
5
+
6
+
7
+ BASE_URL = "https://api.elsevier.com/"
8
+
9
+
10
+ def _ok(query: str, items: list[dict]) -> dict:
11
+ return {
12
+ "ok": True,
13
+ "source": "scopus",
14
+ "query": query,
15
+ "count": len(items),
16
+ "items": items,
17
+ "error": None,
18
+ }
19
+
20
+
21
+ def _err(query: str, message: str) -> dict:
22
+ return {
23
+ "ok": False,
24
+ "source": "scopus",
25
+ "query": query,
26
+ "count": 0,
27
+ "items": [],
28
+ "error": message,
29
+ }
30
+
31
+
32
+ async def _search_scopus(query: str, count: int, sort: str) -> dict:
33
+ api_key = settings.scopus_api_key
34
+ if not api_key:
35
+ raise ValueError("SCOPUS_API_KEY is required")
36
+ headers = {
37
+ "X-ELS-APIKey": api_key,
38
+ "Accept": "application/json",
39
+ "User-Agent": "UniArticlesMCP/0.1.0",
40
+ }
41
+ params = {
42
+ "query": query,
43
+ "count": count,
44
+ "sort": sort,
45
+ "view": "STANDARD",
46
+ }
47
+ async with httpx.AsyncClient(timeout=30.0, headers=headers) as client:
48
+ response = await client.get(f"{BASE_URL}content/search/scopus", params=params)
49
+ response.raise_for_status()
50
+ payload = response.json()
51
+ entries = payload.get("search-results", {}).get("entry", [])
52
+ normalized = []
53
+ for entry in entries:
54
+ normalized.append(
55
+ {
56
+ "title": entry.get("dc:title"),
57
+ "eid": entry.get("eid"),
58
+ "doi": entry.get("prism:doi"),
59
+ "coverDate": entry.get("prism:coverDate"),
60
+ "publicationName": entry.get("prism:publicationName"),
61
+ "creator": entry.get("dc:creator"),
62
+ }
63
+ )
64
+ return _ok(query=query, items=normalized)
65
+
66
+
67
+ def register(server: FastMCP) -> None:
68
+ @server.tool()
69
+ async def search_scopus(query: str, count: int = 5, sort: str = "coverDate") -> dict:
70
+ normalized_query = query.strip()
71
+ bounded = max(1, min(count, 25))
72
+ if not normalized_query:
73
+ return _err(query=query, message="query must not be empty")
74
+ try:
75
+ return await _search_scopus(query=normalized_query, count=bounded, sort=sort)
76
+ except Exception as exc:
77
+ return _err(query=normalized_query, message=str(exc))