paper-search-mcp
# paper-search-mcp
An MCP server exposing scholarly literature search across CrossRef, ERIC,
Semantic Scholar, OpenAlex, and Unpaywall.
## Tools
- `search_crossref(query, rows=20, filter=None, sort=None)` — search CrossRef (all scholarly disciplines)
- `get_crossref_work(doi)` — full CrossRef metadata for one DOI
- `search_eric(query, rows=20, start=0)` — search ERIC (education research literature)
- `get_eric_record(eric_id)` — full ERIC metadata for one record (e.g. `"EJ1234567"`)
- `search_semantic_scholar(query, rows=20)` — search Semantic Scholar (all disciplines)
- `get_semantic_scholar_paper(paper_id)` — full metadata by S2 ID, or `"DOI:..."`, `"ARXIV:..."`, `"PMID:..."`, `"CorpusID:..."`
- `search_openalex(query, rows=20, filter=None, sort=None)` — search OpenAlex (all disciplines)
- `get_openalex_work(work_id)` — full metadata by OpenAlex ID (e.g. `"W2741809807"`) or DOI
- `get_open_access_pdf(doi)` — find a legal open-access PDF for a DOI via Unpaywall
All of these APIs are free and none require an account, except as noted below.
## Configuration
Both environment variables are optional:
| Variable | Effect |
| --- | --- |
| `PAPER_SEARCH_CONTACT_EMAIL` | Sent as a `mailto:` contact to get "polite pool" (faster, more reliable) treatment from CrossRef and OpenAlex. **Required by `get_open_access_pdf`** — Unpaywall rejects requests with no email, and the tool raises a clear error if it is unset. |
| `SEMANTIC_SCHOLAR_API_KEY` | A personal Semantic Scholar key. Without one, S2 calls fall back to the shared unauthenticated rate limit, which is slower and more prone to `429`s. Everything else works unaffected. |
A key enforces 1 request/sec cumulative across all S2 endpoints, so all
Semantic Scholar calls go through a shared throttle (min 1.05s between
requests) plus retry-with-backoff on `429` — the limit is enforced somewhat
burstily in practice.
## Install
Requires Python 3.10+.
### Option A — pipx (recommended)
Installs into an isolated environment and puts a `paper-search-mcp` command
on your `PATH`.
**macOS / Linux**
```bash
brew install pipx # or: python3 -m pip install --user pipx
pipx ensurepath # restart your terminal afterwards
pipx install git+https://github.com/bbutlerau/paper-search-mcp.git
which paper-search-mcp
```
**Windows (PowerShell)**
```powershell
py -m pip install --user pipx
py -m pipx ensurepath # restart PowerShell afterwards
pipx install git+https://github.com/bbutlerau/paper-search-mcp.git
where.exe paper-search-mcp
```
Editable installs (`pipx install -e <path>`) are worth it if you plan to edit
the code or track updates: a `git pull` takes effect immediately with no
reinstall.
### Option B — virtualenv from a clone
**macOS / Linux**
```bash
git clone https://github.com/bbutlerau/paper-search-mcp.git
cd paper-search-mcp
python3 -m venv .venv
.venv/bin/pip install -e .
```
**Windows (PowerShell)**
```powershell
git clone https://github.com/bbutlerau/paper-search-mcp.git
cd paper-search-mcp
py -m venv .venv
.venv\Scripts\pip install -e .
```
### Verify the install
The server takes no command-line arguments; it is configured entirely through
the environment variables above. To confirm it starts, run it and check that
it waits rather than exiting:
```bash
paper-search-mcp # pipx
.venv/bin/paper-search-mcp # venv (macOS/Linux)
.venv\Scripts\paper-search-mcp # venv (Windows)
```
It will sit silently waiting for MCP traffic on stdin — that is correct
behaviour, not a hang. Press Ctrl-C to exit. An immediate traceback (rather
than silence) means the install is broken.
## Connect to Claude Code
```bash
claude mcp add paper-search -s user \
-e PAPER_SEARCH_CONTACT_EMAIL=you@example.com \
-e SEMANTIC_SCHOLAR_API_KEY=<your-key> \
-- paper-search-mcp
```
With a venv instead of pipx, replace the final `paper-search-mcp` with the
absolute path to the launcher inside `.venv`.
Verify with `claude mcp list` — it should report `paper-search: ✓ Connected`.
Remove with `claude mcp remove paper-search -s user`.
## Connect to Claude Desktop
Edit the config file:
- **macOS** — `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows** — `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"paper-search": {
"command": "/absolute/path/to/paper-search-mcp",
"env": {
"PAPER_SEARCH_CONTACT_EMAIL": "you@example.com",
"SEMANTIC_SCHOLAR_API_KEY": "your-key-here"
}
}
}
}
```
Getting `command` exactly right matters:
- **Use an absolute path.** Claude Desktop does not inherit your shell `PATH`,
so a bare `paper-search-mcp` will not resolve.
- **No trailing slash.** A path ending in `/` is treated as a directory and
the spawn fails with `Failed to spawn process: Not a directory`.
- **With pipx, prefer the shim** at `~/.local/bin/paper-search-mcp` over the
venv-internal path (`~/.local/pipx/venvs/…/bin/…`). The shim is what pipx
maintains, and it survives a `pipx reinstall`.
- **On Windows**, use escaped backslashes and the `.exe` suffix, e.g.
`"C:\\Users\\you\\.local\\bin\\paper-search-mcp.exe"`.
Confirm the path resolves to a file before restarting:
```bash
ls -l ~/.local/bin/paper-search-mcp # macOS / Linux
where.exe paper-search-mcp # Windows
```
Then fully quit and reopen Claude Desktop (on macOS ⌘Q — closing the window is
not enough).
### Troubleshooting
If the tools do not appear, check the log:
- **macOS** — `~/Library/Logs/Claude/mcp-server-paper-search.log`
- **Windows** — `%APPDATA%\Claude\logs\mcp-server-paper-search.log`
Two things about that log are easy to misread:
- `Server started and connected successfully` is printed optimistically,
before the process is confirmed running. A `Failed to spawn process` line
immediately after it is the real result.
- An `IncompleteFieldDefinitionWarning` about a `lifespan` field from
`pydantic_settings` is **harmless**. It comes from a dependency on Python
3.14, is written to stderr, and does not touch the JSON-RPC stream on
stdout. Its *absence* alongside a spawn failure is a useful signal: it means
Python never started, so the problem is the `command` path.
Also make sure no stale `paper-search` entry exists under Settings →
Connectors; a duplicate there conflicts with the config-file entry.
## Remote / networked use
`server.py` also supports streamable-HTTP transport, for running the server
on one machine and connecting from another. Running it locally over stdio is
simpler and faster, so prefer that unless you specifically need a shared
instance.
| Variable | Meaning |
| --- | --- |
| `MCP_TRANSPORT` | `stdio` (default) or `streamable-http` |
| `MCP_HOST` / `MCP_PORT` | Bind address (default `127.0.0.1:8000`) |
| `MCP_ALLOWED_HOSTS` | Comma-separated `Host` headers to accept (DNS-rebinding protection); required when behind a reverse proxy |
| `MCP_PUBLIC_URL` | Externally-visible base URL. **When set, enables OAuth 2.1.** Leave unset for local stdio use. |
| `MCP_AUTH_STATE_PATH` | Where to persist OAuth clients/tokens (default: `oauth-state.json` beside `server.py`) |
Bind to loopback and put a reverse proxy in front of it for TLS. Never expose
it directly to the public internet — there is no real authentication here
(see below).
### About the OAuth layer
Claude Desktop's custom-connector flow always attempts OAuth Dynamic Client
Registration against remote MCP servers, even ones advertising no auth, and
there is currently no "no auth" option in the UI (upstream:
[anthropics/claude-ai-mcp#457](https://github.com/anthropics/claude-ai-mcp/issues/457),
[#402](https://github.com/anthropics/claude-ai-mcp/issues/402)). Without an
OAuth implementation, adding the connector fails with "Couldn't register with
… sign-in service."
`auth_provider.py` exists to satisfy that flow. `TrustedNetworkOAuthProvider`
is a minimal OAuth 2.1 authorization server that **auto-approves every client
with no login step**. It is a protocol formality, not access control — it
assumes the server is already reachable only from a trusted network. It
supports Dynamic Client Registration, issues long-lived (1 year) bearer
tokens, and persists clients and tokens to `MCP_AUTH_STATE_PATH` so a restart
does not invalidate cached client registrations.
This entire layer is inert unless `MCP_PUBLIC_URL` is set, so local stdio
installs are unaffected by it.
A `401` from the `/mcp` endpoint when you have no token is the expected
response, and indicates the server is running correctly.
## Development
```bash
pip install -e .
```
The dependency on `mcp` is pinned to `<2`: version 2.x restructured
`mcp.server.fastmcp` and this server targets the 1.x API.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 9 tools
Each tool targets a distinct combination of provider and action (search vs. get), with provider names clearly separating them. The only non-pair tool, get_open_access_pdf, has a unique purpose not covered by the others.
Tool names follow a consistent verb_noun pattern: search_<provider> and get_<provider>_<record> for all four services. The single outlier, get_open_access_pdf, still uses the 'get_' verb style, maintaining overall predictability.
Nine tools is well-scoped for a paper-search server, covering four major scholarly databases plus an OA PDF finder. Every tool provides a distinct, necessary capability without redundancy.
The domain is read-only scholarly search/retrieval, and the set covers the full lifecycle: search any provider, fetch detailed records, and obtain accessible full text. No obvious gaps or dead ends for the stated purpose.