Content
# umlib-mcp
Let your AI assistant read papers using your own University of Michigan library access. Ask it for a paper: it looks for a free copy first, and falls back to pulling the PDF through the U-M proxy using a session you signed into yourself.
Your password never touches the assistant or this server. All that gets kept is the browser session cookie, on your own machine.
Works with Claude Code, Codex, Cursor, VS Code, Zed, Windsurf, Gemini CLI, and Claude Desktop.
## Install
One command. It installs everything and registers it with whichever assistants you already have:
```sh
curl -LsSf https://raw.githubusercontent.com/puppyum/umlib-mcp/main/install.sh | bash
```
Restart your assistant, then say **"log me into the library proxy"** and sign in with your U-M account in the window that opens. That's the whole setup.
macOS and Linux. On Windows, use WSL.
<details>
<summary>Other ways to install it</summary>
Claude Code and Codex share a plugin marketplace, which bundles the usage guidance as a skill:
```
/plugin marketplace add puppyum/umlib-mcp
/plugin install umlib@umlib-lab
```
Use one or the other, not both, or you end up with the server registered twice.
To wire it up by hand, run `uv tool install git+https://github.com/puppyum/umlib-mcp` and point your client at the binary in `uv tool dir --bin`. It takes no arguments. Only the surrounding key changes: `mcpServers` for Cursor, Windsurf, Gemini CLI and Claude Desktop, `servers` for VS Code (which also wants `"type": "stdio"`), `context_servers` for Zed. Claude Desktop needs an absolute path, since it doesn't expand `~`:
```json
{
"mcpServers": {
"umlib": { "command": "/Users/YOU/.local/bin/umlib-mcp" }
}
}
```
To see what the installer would touch without touching anything: `curl -LsSf https://raw.githubusercontent.com/puppyum/umlib-mcp/main/install.sh | bash -s -- --dry-run`
To update: `uv tool upgrade umlib-mcp --reinstall`. The `--reinstall` matters, since a plain upgrade can reuse a cached build. Plugin installs track `@main` and refresh themselves when the server next starts.
</details>
## Use it
Just ask for a paper. No need to wait until you've finished signing in either, since the fetch will sit and wait for you.
- "Get the PDF for doi 10.1145/3411764.3445642 and summarize the methods."
- "Find 'Does Transparency in Moderation Really Matter' by Jhaver et al. and pull the full text."
PDFs save to `~/Downloads`. When a session expires you'll be asked to sign in again. When a publisher blocks the automated fetch, you get a link to open yourself.
## Tools
| Tool | What it does |
|---|---|
| `login` | Opens a browser window so you can sign in. Returns right away; the session is saved for later. |
| `auth_status` | Says whether a session is active. |
| `resolve` | Turns a DOI or title into metadata, a free-copy link, and the proxied publisher URL. |
| `fetch_pdf` | Fetches one PDF (free copy first, then the proxy) and saves it to `~/Downloads`. |
| `proxy_url` | Prefixes a URL for off-campus access. |
| `logout` | Deletes the saved session. |
## Making it reach for the tools on its own
Out of the box your assistant will use these tools when you ask it to. To have it reach for them on its own, paste this into your project instructions (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`):
> When searching for or downloading scholarly articles, papers, books or other publications, use the umlib tools. If content looks inaccessible (paywalled, login-walled, 403, or abstract-only) try umlib's `fetch_pdf` before concluding it is unavailable, since it may be accessible through the authenticated library session. Fetch one paper at a time.
If you installed through the plugin instead of the script, that same text ships as a skill and you can skip this step.
## One rule
Fetch papers as you need them, not in bulk. The library's [appropriate-use policy](https://lib.umich.edu/about-us/policies/statement-appropriate-use-electronic-resources) forbids systematic downloading, and publishers respond by cutting off the whole campus rather than one account. The policy sets no number, so the 60-an-hour default here is our own courtesy limit: well above ordinary reading, well below anything that looks like a crawler. For text or data mining at scale, talk to library-ds@umich.edu.
## Configuration
Everything has a working default, so you can skip this. To change something, write `~/.umlib/config.toml`:
```toml
max_fetches_per_hour = 60 # courtesy cap on licensed fetches
download_dir = "~/Papers" # where PDFs land
email = "you@umich.edu" # adds Unpaywall alongside OpenAlex
```
| Setting | Default | Purpose |
|---|---|---|
| `max_fetches_per_hour` | `60` | Courtesy cap on licensed fetches. `0` switches licensed fetching off entirely |
| `min_fetch_interval_s` | `5` | Seconds between licensed fetches |
| `email` | unset | Optional. Free-copy checks use OpenAlex and need no email; setting one adds Unpaywall as a second source |
| `download_dir` | `~/Downloads` | Where PDFs save |
| `profile_dir` | `~/.umlib/profile` | Where the session is stored |
| `proxy_base` | `https://proxy.lib.umich.edu/login?url=` | EZproxy prefix. Must be `https`, or the session cookie would travel in the clear |
| `canary_url` | `https://www.jstor.org/` | The site used to tell "signed in" from "signed out". Point it at something your library definitely licenses |
| `resolver_base` | U-M's MGet It | Link resolver for the "how do I get this" link; set it to `""` to omit it |
| `rewrite_host` | derived from `proxy_base` | Only needed if your proxy signs in on one domain and rewrites onto another |
| `max_pdf_bytes` | `104857600` | Refuse anything larger before saving |
| `login_timeout_s` | `300` | How long the sign-in window waits for you |
| `login_wait_s` | `150` | How long a fetch waits for a sign-in already in progress |
| `lock_timeout_s` | `login_timeout_s + 60` | How long to wait for another assistant's browser to finish |
Two more inputs come from the environment only: `UMLIB_CONFIG` moves the config file itself, and `XDG_DOWNLOAD_DIR` supplies the default download directory on Linux desktops that set it. Every setting also reads from an environment variable (`UMLIB_MAX_FETCHES_PER_HOUR` and so on), which wins over the file; the file exists because environment variables are awkward to apply across assistants.
If umlib can't use a setting it says so in `auth_status`, under `config_error`, instead of quietly falling back. Relative paths resolve against your home directory rather than wherever your assistant happened to start.
It works at other EZproxy schools, but check three settings before trusting it: `proxy_base`, `canary_url` (the default is JSTOR, and a school that doesn't license JSTOR can never look signed in), and `resolver_base` (the default points at U-M's resolver). OCLC-hosted proxies are detected automatically.
## Develop
```sh
uv sync
uv run pytest
uv run python scripts/smoke.py # runs the server over stdio and exercises it
```
A stdio server doesn't reload while it's running, so restart it (`/mcp` in Claude Code) after editing the source. The plugin tracks `@main`, so everyone picks up changes the next time their server starts.
## License
MIT