Skip to main content
Glama
bbutlerau

paper-search-mcp

by bbutlerau
README.md
# 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

A4.2/5.0

Scored across 9 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues