Scopus MCP Server
# Scopus MCP Server
An [MCP](https://modelcontextprotocol.io) server that wraps the Elsevier Scopus API so an MCP
client (Claude Desktop, Claude Code, or any other MCP host) can search for and retrieve published
academic articles — useful for citation verification and writing-style analysis grounded in real,
peer-reviewed sources.
## Tools
| Tool | Input | What it returns |
|---|---|---|
| `search_scopus` | `query` (author, keywords, title, or DOI), optional `count` (1–25, default 10) | Up to `count` articles: title, authors, publication year, abstract (if Scopus includes one in search results), source title, DOI, DOI URL, Scopus ID, cited-by count |
| `get_article_details` | `scopusId` | Full metadata for one article: everything above plus author keywords, subject areas, open-access flag, aggregation type |
| `get_article_abstract` | `scopusId` | Just the abstract text for one article, plus `hasAbstract: false` when Scopus has none on file |
All responses are structured JSON (see [Response shape](#response-shape) below). Every tool
returns a friendly, structured error instead of throwing when the Scopus API is unreachable,
rate-limited, or given a bad ID — see [Error handling](#error-handling).
Under the hood the server calls two Elsevier APIs:
- **Scopus Search API** (`GET /content/search/scopus`) — used by `search_scopus`.
- **Abstract Retrieval API** (`GET /content/abstract/scopus_id/{id}`) — used by
`get_article_details` and `get_article_abstract`, since the Search API does not reliably return
full abstracts, citation counts, or keywords.
## Project layout
```
mcp-server/
├── src/
│ ├── index.ts # stdio entry point (for local MCP clients)
│ ├── httpServer.ts # Streamable HTTP entry point (for remote deployment)
│ ├── registerTools.ts # tool definitions, shared by both entry points
│ ├── scopusClient.ts # Elsevier API client: requests, normalization, error mapping
│ ├── types.ts # TypeScript types for raw Scopus responses + normalized output
│ └── logger.ts # structured logger → stderr + logs/scopus-mcp.log
├── test/
│ └── test-connection.ts # standalone connectivity test (bypasses the MCP protocol)
├── logs/ # log file written here at runtime (gitignored)
├── .env.example
├── package.json
└── tsconfig.json
```
## Prerequisites
- **Node.js 18 or later** (uses the built-in global `fetch`). Check with `node -v`.
- **A Scopus API key.** Register a free key at the
[Elsevier Developer Portal](https://dev.elsevier.com/apikey/manage). Note that Elsevier gates
full-text/abstract access by IP range (institutional subscription) or Institutional Token — a
key alone is enough to test connectivity and basic search, but some fields may be limited
depending on your entitlements.
## Setup
```bash
cd mcp-server
npm install
cp .env.example .env
```
Edit `.env` and set your key:
```
SCOPUS_API_KEY=your_real_key_here
```
`SCOPUS_API_KEY` is read from the environment at startup (`src/scopusClient.ts`); it is never
hard-coded and `.env` is gitignored so it can't be committed by accident.
### Environment variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
| `SCOPUS_API_KEY` | ✅ | — | Your Elsevier Scopus API key |
| `SCOPUS_INST_TOKEN` | optional | — | Institutional Token, if your key needs one for off-campus access |
| `SCOPUS_API_BASE_URL` | optional | `https://api.elsevier.com` | Override for testing against a proxy/mock |
| `SCOPUS_REQUEST_TIMEOUT_MS` | optional | `15000` | Per-request timeout |
| `LOG_LEVEL` | optional | `info` | `debug` \| `info` \| `warn` \| `error` |
| `PORT` | HTTP mode only | `3000` | Port for `httpServer.ts` (most hosts set this for you) |
| `HOST` | HTTP mode only | `0.0.0.0` | Bind address for `httpServer.ts` |
| `MCP_HTTP_AUTH_TOKEN` | HTTP mode, strongly recommended | — | If set, `/mcp` requires `Authorization: Bearer <token>` |
| `MCP_ALLOWED_HOSTS` | HTTP mode, optional | — | Comma-separated `Host` header allowlist (DNS-rebinding protection) |
## Test connectivity first
Before wiring the server into any MCP client, verify the Scopus API key and network path work:
```bash
npm run test:connection
```
This runs `test/test-connection.ts`, which calls the same client functions the tools use — but
directly, without speaking the MCP protocol — against the sample query **"farmland abandonment
Nepal"**. You can pass your own query instead:
```bash
npm run test:connection -- "AUTH(Smith J) AND TITLE(remote sensing)"
```
It walks through all three tools in sequence (search → details → abstract for the first result)
and prints ✅/❌ per step, plus a full request/response log at `logs/scopus-mcp.log` (see
[Logging](#logging)). Exit code is `0` only if every step succeeded.
## Running locally (stdio, for a local MCP client)
```bash
npm run dev # runs src/index.ts directly via tsx, no build step
# or
npm run build && npm start # compiles to dist/ then runs the compiled server
```
The server communicates over stdio, so running it directly in a terminal will just sit there
waiting for JSON-RPC on stdin — that's expected. It's meant to be launched by an MCP client.
### Connect it to Claude Code
```bash
claude mcp add scopus --env SCOPUS_API_KEY=your_real_key_here -- node /absolute/path/to/mcp-server/dist/index.js
```
(run `npm run build` first so `dist/index.js` exists), or add it to a project's `.mcp.json`:
```json
{
"mcpServers": {
"scopus": {
"command": "node",
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
"env": { "SCOPUS_API_KEY": "your_real_key_here" }
}
}
}
```
### Connect it to Claude Desktop
Add the same block to `claude_desktop_config.json`
(`%APPDATA%\Claude\claude_desktop_config.json` on Windows,
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS), then restart Claude
Desktop:
```json
{
"mcpServers": {
"scopus": {
"command": "node",
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
"env": { "SCOPUS_API_KEY": "your_real_key_here" }
}
}
}
```
## Response shape
`search_scopus` example (truncated):
```json
{
"query": "farmland abandonment Nepal",
"totalResults": 42,
"returnedResults": 10,
"articles": [
{
"scopusId": "85123456789",
"eid": "2-s2.0-85123456789",
"title": "Drivers of farmland abandonment in the mid-hills of Nepal",
"authors": ["Sharma B.", "Poudel K."],
"publicationYear": 2021,
"sourceTitle": "Land Use Policy",
"doi": "10.1016/j.landusepol.2021.105123",
"doiUrl": "https://doi.org/10.1016/j.landusepol.2021.105123",
"scopusUrl": "https://www.scopus.com/inward/record.uri?...",
"citedByCount": 17,
"abstract": null,
"documentType": "Article"
}
]
}
```
`get_article_details` adds `keywords`, `subjectAreas`, `openAccess`, and `aggregationType` on top
of the same fields. `get_article_abstract` returns `{ scopusId, title, abstract, hasAbstract }`.
Fields Scopus doesn't have for a given record come back as `null` (or `[]` for list fields, or
`hasAbstract: false`) rather than being omitted — check for `null`/`false` before assuming a field
is missing due to a bug.
## Error handling
Every tool catches errors internally and returns `isError: true` with a structured JSON body
instead of crashing the MCP connection:
```json
{
"error": true,
"kind": "rate_limited",
"message": "Scopus API rate limit exceeded (HTTP 429) for search_scopus(...). Retry after 30s.",
"status": 429,
"retryAfterSeconds": 30
}
```
`kind` is one of: `unauthorized` (bad/missing API key), `rate_limited` (HTTP 429),
`not_found` (bad Scopus ID / HTTP 404), `bad_request` (empty query, malformed input),
`network_error` (DNS/connection failure), `timeout` (exceeded `SCOPUS_REQUEST_TIMEOUT_MS`), or
`unknown`. A search that succeeds but matches nothing is **not** an error — it returns
`totalResults: 0` and a human-readable `message` suggesting how to broaden the query.
## Logging
All API calls and responses are logged for debugging:
- Every request logs its URL (API key redacted) before it's sent.
- Every response logs status code, elapsed time, and a 500-character body preview.
- Logs go to **stderr** as single-line JSON (never stdout — stdout is reserved for the MCP
protocol on the stdio transport) and are also appended to `logs/scopus-mcp.log`.
- Set `LOG_LEVEL=debug` for more detail, or `LOG_LEVEL=error` to quiet things down.
## Deploying to a remote/serverless platform (Render, Railway, etc.)
The stdio transport (`src/index.ts`) only works for MCP clients that can spawn a local process —
it's not reachable over the network. To host this server remotely, use the **Streamable HTTP**
entry point instead: `src/httpServer.ts`. It serves the same three tools at `POST /mcp` and adds a
`GET /healthz` endpoint for the platform's health checks.
> Neither Render nor Railway is truly "serverless" (no scale-to-zero cold starts mid-request) —
> both run this as a normal persistent Node process, which is what a stateful protocol like MCP
> needs. Treat "serverless platform" here as "managed Node hosting."
### Render
1. Push this repo (or just the `mcp-server/` folder) to GitHub.
2. In the Render dashboard: **New → Web Service**, connect the repo, set **root directory** to
`mcp-server` if it's a subfolder of a larger repo.
3. **Build command:** `npm install && npm run build`
4. **Start command:** `npm run start:http`
5. Under **Environment**, add:
- `SCOPUS_API_KEY` = your key (mark it as a secret)
- `MCP_HTTP_AUTH_TOKEN` = a long random string you generate (e.g. `openssl rand -hex 32`)
- optionally `MCP_ALLOWED_HOSTS` = your Render hostname, e.g. `scopus-mcp.onrender.com`
6. Render sets `PORT` automatically — `httpServer.ts` reads it, no action needed.
7. Deploy. Health check path: `/healthz`.
### Railway
1. **New Project → Deploy from GitHub repo**, set the service root to `mcp-server` if needed.
2. Railway auto-detects Node; if it doesn't run the right command, set:
- **Build command:** `npm install && npm run build`
- **Start command:** `npm run start:http`
3. In **Variables**, add `SCOPUS_API_KEY` and `MCP_HTTP_AUTH_TOKEN` as above.
4. Railway injects `PORT` automatically.
5. Once deployed, your MCP endpoint is `https://<your-app>.up.railway.app/mcp`.
### Connecting an MCP client to the hosted server
```bash
claude mcp add --transport http scopus https://<your-app>/mcp \
--header "Authorization: Bearer <your MCP_HTTP_AUTH_TOKEN>"
```
### Security notes for HTTP deployment
- **Always set `MCP_HTTP_AUTH_TOKEN`.** Without it, anyone with the URL can call your tools and
consume your Scopus API quota — the server logs a startup warning if it's unset.
- The server binds DNS-rebinding protection automatically for `localhost`/`127.0.0.1`; for a real
`0.0.0.0` deployment, set `MCP_ALLOWED_HOSTS` to your platform's hostname.
- Rotate `SCOPUS_API_KEY` and `MCP_HTTP_AUTH_TOKEN` via your platform's secret manager, never by
committing them to the repo.
- Consider putting the platform's own rate limiting / a reverse-proxy in front for public
deployments, on top of Elsevier's own per-key rate limits.
## Troubleshooting
| Symptom | Likely cause |
|---|---|
| `SCOPUS_API_KEY is not set` | `.env` missing/not loaded, or you're running in a shell that doesn't have it exported |
| `kind: "unauthorized"`, HTTP 401/403 | Invalid key, or key lacks Scopus Search entitlements, or missing `SCOPUS_INST_TOKEN` for off-campus access |
| `kind: "rate_limited"`, HTTP 429 | Elsevier's per-key rate/quota limit hit — back off and retry after `retryAfterSeconds` |
| `kind: "not_found"`, HTTP 404 | The `scopusId` doesn't exist or was mistyped |
| `kind: "network_error"` / `"timeout"` | No internet access from this machine/host, corporate proxy blocking `api.elsevier.com`, or `SCOPUS_REQUEST_TIMEOUT_MS` too low |
| Tool calls silently do nothing in a stdio client | Something wrote to **stdout** — check you haven't added a stray `console.log`; use `logger` (stderr) instead |
## License
MIT
TDQS
Scored across 3 tools
The three tools have clearly distinct primary actions: searching versus retrieving by Scopus ID. However, get_article_details and get_article_abstract overlap since details also includes the abstract when available, which could cause minor confusion about which to call.
All tool names follow a consistent verb_noun pattern: search_scopus, get_article_details, get_article_abstract. The get_article_* prefix for the two retrieval tools reinforces a predictable structure.
Three tools is on the small side but appropriately scoped for a simple search-and-retrieve workflow. The count feels slightly thin for a general-purpose Scopus API wrapper, but each tool serves a necessary step in the primary flow.
The core lifecycle of discovering articles and retrieving full metadata or abstracts is covered, with no dead ends. Minor gaps exist, such as no direct citation-list or author-detail endpoints, but these are reasonable omissions given the stated purpose.