Read-only MCP server for LibGuides sites, built on the Springshare LibApps API v1.2. It lets an AI assistant browse published research guides (including every page and sub-page as clean markdown), guide subjects, the A-Z database list, and public subject-librarian profiles. Runs over stdio.
Nothing institution-specific is built in: the API region host, public site URL,
and credentials are all configuration. Without credentials the server still
starts, and every tool returns config_missing.
- Write anything (no create/update tools; the app needs GET scopes only)
- Show unpublished guides, Internal or Template guides, or draft content
- Index content or keep its own search index
- Other Springshare products (LibCal, LibAnswers, LibWizard)
- Hosted / remote access (see Phase 2)
- A LibGuides CMS site
- A read-only LibApps v1.2 application with GET scopes for Guides, Subjects, A-Z, Accounts, and Assets
- uv and Python 3.11+
In LibApps, go to Tools > API > Applications and create (or reuse) an
application with only the GET scopes above. That page also shows your
region's API host (for example lgapi-us.libapps.com); use it for
LIBAPPS_API_BASE.
Keep the client secret out of git, tickets, and chat. Put it in your MCP host
config or a local, git-ignored env file. .env.example lists the variables
with placeholder values. Rotate the secret if it is ever exposed.
cd /path/to/libapps-mcp
uv sync --extra devPoint Claude Desktop (or another stdio MCP host) at the checkout with
uv run --directory. Replace /path/to/libapps-mcp with your clone path
and the placeholder values with your own:
{
"mcpServers": {
"libapps": {
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/libapps-mcp",
"python",
"-m",
"libapps_mcp"
],
"env": {
"LIBAPPS_API_BASE": "https://lgapi-us.libapps.com",
"LIBAPPS_CLIENT_ID": "your-client-id",
"LIBAPPS_CLIENT_SECRET": "your-client-secret",
"LIBGUIDES_SITE_URL": "https://guides.lib.uchicago.edu"
}
}
}
}You can also run the console script uv run libapps-mcp from a shell for a
quick smoke check.
All settings are environment variables. They are read at the first tool call.
| Variable | Default | Purpose |
|---|---|---|
LIBAPPS_CLIENT_ID / LIBAPPS_CLIENT_SECRET |
(required) | OAuth client credentials for the v1.2 app |
LIBAPPS_API_BASE |
https://lgapi-us.libapps.com |
Region host: lgapi-{us,ca,eu,au}.libapps.com. Other hosts need LIBAPPS_ALLOW_CUSTOM_API_BASE=1 |
LIBGUIDES_SITE_URL |
derived from guide URLs | Public site origin, used for page fetches |
LIBGUIDES_ALLOWED_HOSTS |
(none) | Extra comma-separated hosts the page fetcher may use |
LIBAPPS_HTML_FETCH |
1 |
Read page content from public guide pages. 0 falls back to unplaced API text |
LIBAPPS_INCLUDE_PRIVATE |
0 |
Include Private guides and hidden pages/boxes |
LIBAPPS_EXPOSE_EMAIL |
0 |
Include the public-profile contact email for librarians |
LIBAPPS_CACHE_TTL_LIST / _REF / _CONTENT |
1800 / 86400 / 900 |
Cache lifetimes (seconds) for the guide list; subjects, accounts, and A-Z; and guide outlines, pages, and searches |
LIBAPPS_TIMEOUT |
30 |
HTTP timeout (seconds) |
LIBAPPS_MAX_CONCURRENCY |
4 |
Maximum concurrent upstream requests |
LIBAPPS_MAX_CHARS |
20000 |
Default markdown budget per get_guide_content call |
LIBAPPS_USER_AGENT |
libapps-mcp/0.1.0 (+repo URL) |
User agent for all requests |
LIBAPPS_LOG_LEVEL |
WARNING |
Log level; logs go to stderr only |
Every tool returns {"ok": true, ...} or
{"ok": false, "error": "...", "code": "..."}. Codes: config_missing,
config_invalid, auth_failed, scope_missing, not_found, not_public,
upstream_error, rate_limited, fetch_blocked, invalid_input. List tools
take limit (max 50) and offset and return total and next_offset
(null when done).
search_guidesfinds a guide.get_guide(guide_id)returns its metadata and the page tree: every visible page and sub-page withpage_id,name,url, and box names.get_guide_content(guide_id, page_id, include_subpages)returns that page as markdown, one entry per content box in display order. Withinclude_subpages=trueit adds the page's sub-pages. Each response also includes the fulloutline, so the assistant can move to any other page.
| Argument | Notes |
|---|---|
query |
Uses the site's relevance-ranked guide search (source: "server"). Without a query, or if that search fails, matches name, subjects, description, and owner locally (source: "local") |
subject |
Subject name (substring) or id |
owner |
Owner name; only owners with a public profile can match |
guide_type |
subject, course, topic, general, or a type id |
limit / offset |
Default 10 / 0 |
Returns id, name, url, description, type, subjects, updated,
rank, and owner (name and profile link) when the owner has a public profile.
| Argument | Notes |
|---|---|
guide_id |
Numeric guide id |
Pages have page_id, name, url, parent_id, box_count, boxes
(box_id, name, column, position), and subpages. Link pages carry
redirect_url and are never followed.
| Argument | Notes |
|---|---|
guide_id |
Numeric guide id |
page_id |
Defaults to the guide's first page |
include_subpages |
Add the page's sub-pages (default false) |
source |
auto (default), html (public page), or api (whole-guide text blocks with placement: "unknown") |
max_chars |
Markdown budget (default LIBAPPS_MAX_CHARS, max 100000) |
offset |
Box index to resume from (use next_offset) |
Boxes that are in the outline but not on the page are marked missing. A box
cut to fit the budget is marked truncated. Embedded widgets become
[embedded: ...] placeholders.
with_published_guides (default true) limits the list to subjects that have
published guides. Returns id, name, slug, and parent_id.
| Argument | Notes |
|---|---|
query |
Matches name, alternate names, vendor, and description |
subject / az_type |
Name (substring) or id |
limit / offset |
Default 20 / 0 |
Returns id, name, url, proxied, vendor, a short description,
subjects, types, and alt_names / new / trial / popular when available.
database_id: the same fields with the full description and more_info.
subject: name or id. Returns librarians with a public profile for that
subject: name, title, pronouns, profile_url, image_url, subjects,
and the contact details (phone, address, website) they chose to show
publicly.
- Published only. Unpublished, Internal, and Template guides are never
returned. Private guides and hidden pages/boxes need
LIBAPPS_INCLUDE_PRIVATE=1. - Allowlisted output. Every result is built field by field. Login emails,
account ids, widget code, and A-Z internal notes and reviews are never
returned. Librarians without a public profile are excluded. Contact details
appear only when the profile shows them, and the profile email only with
LIBAPPS_EXPOSE_EMAIL=1. - No email addresses by default. Unless
LIBAPPS_EXPOSE_EMAIL=1, email addresses in page text and descriptions are replaced with[email removed]. - Secrets stay private. The client secret and access token are never logged
or returned; settings
repr()hides the credentials. - Safe page fetches. Only
httpsURLs from API data, on the site host orLIBGUIDES_ALLOWED_HOSTS. Redirects are re-checked (max 3), pages are capped at 2 MB and must betext/html, and no cookies or auth headers are sent. - Polite client. Cached responses, a concurrency cap,
gzip, and backoff on 429/5xx.
uv run pytestUnit tests mock all HTTP with synthetic fixtures. An opt-in live smoke test reads credentials from the environment and asserts only on shapes and counts:
LIBAPPS_LIVE_TESTS=1 uv run pytest -m liveNotes on the API behaviors this server relies on are in
docs/api-notes.md.
To verify a running server end to end against the live API (stdio startup,
every tool, privacy and status filters, error shapes), follow
.claude/skills/verify-libapps-mcp/SKILL.md.
Its driver reads credentials from ~/.config/libapps-mcp/env. If only
~/.config/uc-libapps-mcp/env exists (the path used before this project was
renamed from uc-libapps-mcp), it reads that instead.
A later phase adds a streamable HTTP transport served at /mcp behind TLS and
an authenticating proxy, with per-deployment LibApps credentials kept
server-side.
MIT