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.
- uniarticles_mcp-0.1.0/.env.example +2 -0
- uniarticles_mcp-0.1.0/PKG-INFO +181 -0
- uniarticles_mcp-0.1.0/README.md +154 -0
- uniarticles_mcp-0.1.0/claude_desktop_config.example.json +13 -0
- uniarticles_mcp-0.1.0/pyproject.toml +47 -0
- uniarticles_mcp-0.1.0/src/uniarticles/__init__.py +3 -0
- uniarticles_mcp-0.1.0/src/uniarticles/__main__.py +10 -0
- uniarticles_mcp-0.1.0/src/uniarticles/config.py +16 -0
- uniarticles_mcp-0.1.0/src/uniarticles/server.py +9 -0
- uniarticles_mcp-0.1.0/src/uniarticles/sources/__init__.py +13 -0
- uniarticles_mcp-0.1.0/src/uniarticles/sources/arxiv.py +62 -0
- uniarticles_mcp-0.1.0/src/uniarticles/sources/chemrxiv.py +73 -0
- uniarticles_mcp-0.1.0/src/uniarticles/sources/scopus.py +77 -0
- uniarticles_mcp-0.1.0/src/uniarticles/sources/semanticscholar.py +58 -0
- uniarticles_mcp-0.1.0/tests/test_integration.py +78 -0
- uniarticles_mcp-0.1.0/tests/verify_server.py +131 -0
- uniarticles_mcp-0.1.0/uv.lock +1038 -0
|
@@ -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,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,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,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))
|