MCP server for Project Gutenberg: 78,000+ public-domain ebooks with full plain-text retrieval.
One address for any MCP client. No sign-in needed on this server.
4 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
Search, browse, and read 78,000+ public-domain books from Project Gutenberg with full plain-text retrieval and offset/limit chunking via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://gutenberg.caseyjhand.com/mcp
Project Gutenberg's library of 78,000+ public-domain books. Search by title, author, topic, or language, fetch full book metadata, and read supported UTF-8 plain-text and HTML editions in manageable chunks.
| Tool | Description |
|---|---|
gutenberg_search_books | Search the catalog by title, author, topic, language, or author lifespan, with pagination and popularity sorting |
gutenberg_get_book | Fetch complete metadata for a book by ID — authors, translators, editors, full formats map, subjects, copyright status |
gutenberg_get_text | Retrieve a book's plain-text content stripped of license boilerplate, with offset/limit chunking |
gutenberg_browse_popular | Browse the most-downloaded books, optionally filtered by language or topic |
gutenberg_search_books toolquery (title/author words), topic, languages (ISO 639-1), author_year_start/author_year_end, ids, mime_type, and copyright ("true", "false", "null"; false means public domain in the USA). Live fallback rejects queries over four wordssort is popular (default) or ascending/descending by Gutenberg ID. Up to 32 books per page; use totalCount and hasMore to pageis_readable (readable by gutenberg_get_text) and has_plain_text. Fails with query_requires_mirror, page_out_of_range, or catalog_unavailablegutenberg_get_book toolformats map (MIME type → download URL) and authors, translators, and editors with birth/death yearsis_readable, has_plain_text, summary, source_notes, and download_count (downloads in the last 30 days, not lifetime)not_found or catalog_unavailablegutenberg_get_text tooloffset/limit chunks (default limit 20,000, capped at 50,000), with the Gutenberg license header and footer stripped. requestedLimit and appliedLimit report bothsourceFormat says which. Returns totalChars, length, remainingChars, hasMore, and provenance (ID, title, license URL). Use length, not limit, to compute the next offsetnot_found, audio_book (media_type "Sound"), no_text_format, offset_out_of_range, text_file_not_found, text_fetch_rejected, invalid_text_response, text_fetch_failed, or catalog_unavailablegutenberg_browse_popular toollimit, default 20), most downloaded in the last 30 days first. requestedLimit and appliedLimit report the caplanguages, topic, mime_type, and copyright filters work as in search. totalInCatalog gives the full countcontinuation pointing at gutenberg_search_books. Empty results succeed with a notice; fails with catalog_unavailableBuilt on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Gutenberg-specific:
Agent-friendly output:
is_readable on every search and browse result flags editions gutenberg_get_text can readoffset, length, totalChars, remainingChars, and hasMore support reliable sequential readsprovenance and sourceFormat on every text response cover attribution and fidelityA public instance is available at https://gutenberg.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"gutenberg-mcp-server": {
"type": "streamable-http",
"url": "https://gutenberg.caseyjhand.com/mcp"
}
}
}
No API key required. Add the following to your MCP client configuration file:
{
"mcpServers": {
"gutenberg-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/gutenberg-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"gutenberg-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/gutenberg-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"gutenberg-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/gutenberg-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/gutenberg-mcp-server.git
cd gutenberg-mcp-server
bun install
cp .env.example .env
# edit .env if you need to override any defaults
| Variable | Description | Default |
|---|---|---|
GUTENDEX_BASE_URL | Gutendex catalog API base URL. | https://gutendex.com/books/ |
GUTENBERG_TEXT_BASE_URL | Content mirror serving the /cache/epub file tree. | https://gutenberg.pglaf.org |
GUTENBERG_MIRROR_PATH | SQLite catalog path; use a persistent volume in containers. | ./.mirror/gutenberg-catalog.sqlite |
GUTENBERG_MIRROR_REFRESH_CRON | Cron schedule for in-process catalog refresh. | Unset |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. Overrides the stateless posture the server declares in createApp(). | stateless |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
Build the local catalog with bun run mirror:init and check it with bun run mirror:verify. The server never harvests on startup, and uses Gutendex until the first harvest completes. Keep the catalog up to date with bun run mirror:refresh on a host scheduler, or set GUTENBERG_MIRROR_REFRESH_CRON.
Build and run:
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
docker build -t gutenberg-mcp-server .
docker run --rm -p 3010:3010 gutenberg-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/gutenberg-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools and inits services. |
src/config/server-config.ts | Server-specific environment variable parsing (Gutendex and file-server URL overrides). |
src/mcp-server/tools/definitions/ | Tool definitions (*.tool.ts). |
src/services/gutendex/ | Gutendex catalog API client — search and book metadata. |
src/services/catalog-mirror/ | Local SQLite+FTS5 catalog mirror — RDF ingestion and read helpers. |
src/services/gutenberg-text/ | Full plain-text retrieval, boilerplate stripping, tenant-scoped caching, and chunking. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md / AGENTS.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Data from Project Gutenberg is in the public domain. Catalog metadata sourced from Gutendex (MIT license).
Apache-2.0 — see LICENSE for details.