Skip to content
goodread

The MCP server

Serve public Goodreads data to a model over stdio, with eleven read tools and three deliberate omissions.

goodread mcp

That is an MCP server speaking JSON-RPC over stdio. Point a client at it and a model can read books, works, authors, series, editions, quotes, genres and lists, and search your local store.

Wiring it up

Most clients take a command and its arguments. For Claude Desktop or any client with the same shape:

{
  "mcpServers": {
    "goodread": {
      "command": "goodread",
      "args": ["mcp", "--store", "/Users/you/books.db"]
    }
  }
}

The store is optional. A server started before anything has been crawled works fine, and the two store tools say so themselves rather than the command refusing to start.

The startup notice goes to stderr, because stdout is the transport and a notice printed onto the wire would be a protocol error rather than a notice.

The eleven tools

Tool Reads
book_get one book, by id, ISBN or URL
work_get one work, through its best edition
author_get one author
series_get one series
editions_list the editions of a work
quotes_list a work's or an author's quotes
genre_get a genre, its featured books and its related genres
list_get a Listopia list
book_lookup an identifier resolved to a book
store_find full text search over the local store
store_query one read only SQL statement over the local store

Every tool has an input schema with at least one required argument, so an empty call is invalid rather than expensive. Every one takes an id, a bare number or a full Goodreads URL, because somebody pasting a URL into a tool call is the common case and refusing it would cost a round trip to explain something the tool can just do.

store_query is read only. delete, update, drop and insert are refused.

The three it does not have

No search, no shelf, no reviews.

Those are the surfaces robots.txt disallows, and the CLI will read them if you pass --no-robots. The server will not, and --no-robots has no effect on it even when the process was started with the flag set.

The reason is not that the flag is wrong. It is that the flag's whole justification is a person deciding it is their call, and a model calling a tool is not that person deciding. An override a model can trigger is not an override, it is a default.

This is enforced rather than documented. A test walks the tool list against the op registry and fails the build if any tool reads a surface marked disallowed, and a second test checks the names, because a surface check alone would pass for a tool that reached a disallowed page through an op registered later without the flag on it.

The server also builds its own client from a config with the override taken back out, so even a tool added tomorrow that pointed at a disallowed path would be refused by the client rather than warned about and fetched.

Errors

A tool that fails answers with a result carrying isError, not a JSON-RPC error. A book that does not exist is a normal answer to a reasonable question, and a model that gets a protocol error for it learns the wrong lesson.

A JSON-RPC error is reserved for the protocol going wrong, like a method the server does not implement.

Asking for a tool that does not exist gets a message naming the tools that do, since the likeliest reason for asking is one of the three deliberate omissions.

Pace

The server is the same client as everything else. One request at a time, two seconds apart by default, one second floor, and the same on-disk cache, so a model asking for the same book twice costs one request.