From b30baa677e66dec29020728c31de404591364a78 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 29 Apr 2025 00:51:35 +0100 Subject: [PATCH 1/5] feat: add get-book-by-id tool --- src/index.test.ts | 8 +- src/index.ts | 24 +++++ src/tools/get-book-by-id/index.ts | 143 ++++++++++++++++++++++++++++++ src/tools/get-book-by-id/types.ts | 114 ++++++++++++++++++++++++ src/tools/index.ts | 1 + 5 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 src/tools/get-book-by-id/index.ts create mode 100644 src/tools/get-book-by-id/types.ts diff --git a/src/index.test.ts b/src/index.test.ts index ac9e9bd..6ce7fe6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -63,7 +63,7 @@ describe("OpenLibraryServer", () => { if (listToolsHandler) { const result = await listToolsHandler({} as any); // Call the handler - expect(result.tools).toHaveLength(5); + expect(result.tools).toHaveLength(6); expect(result.tools[0].name).toBe("get_book_by_title"); expect(result.tools[0].description).toBeDefined(); expect(result.tools[0].inputSchema).toEqual({ @@ -91,7 +91,7 @@ describe("OpenLibraryServer", () => { if (listToolsHandler) { const result = await listToolsHandler({} as any); - expect(result.tools).toHaveLength(5); + expect(result.tools).toHaveLength(6); const authorTool = result.tools.find( (tool: any) => tool.name === "get_authors_by_name", ); @@ -176,7 +176,7 @@ describe("OpenLibraryServer", () => { if (listToolsHandler) { const result = await listToolsHandler({} as any); - expect(result.tools).toHaveLength(5); + expect(result.tools).toHaveLength(6); const authorInfoTool = result.tools.find( (tool: any) => tool.name === "get_author_info", ); @@ -248,7 +248,7 @@ describe("OpenLibraryServer", () => { if (listToolsHandler) { const result = await listToolsHandler({} as any); - expect(result.tools).toHaveLength(5); + expect(result.tools).toHaveLength(6); const authorPhotoTool = result.tools.find( (tool: any) => tool.name === "get_author_photo", ); diff --git a/src/index.ts b/src/index.ts index a74961e..ccac004 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { handleGetBookCover, handleGetAuthorsByName, handleGetAuthorInfo, + handleGetBookById, } from "./tools/index.js"; class OpenLibraryServer { @@ -138,6 +139,27 @@ class OpenLibraryServer { required: ["key", "value"], }, }, + { + name: "get_book_by_id", + description: + "Get detailed information about a book using its identifier (ISBN, LCCN, OCLC, OLID).", + inputSchema: { + type: "object", + properties: { + idType: { + type: "string", + enum: ["isbn", "lccn", "oclc", "olid"], + description: + "The type of identifier used (ISBN, LCCN, OCLC, OLID).", + }, + idValue: { + type: "string", + description: "The value of the identifier.", + }, + }, + required: ["idType", "idValue"], + }, + }, ], })); @@ -155,6 +177,8 @@ class OpenLibraryServer { return handleGetAuthorPhoto(args); case "get_book_cover": return handleGetBookCover(args); + case "get_book_by_id": + return handleGetBookById(args, this.axiosInstance); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } diff --git a/src/tools/get-book-by-id/index.ts b/src/tools/get-book-by-id/index.ts new file mode 100644 index 0000000..e1bafe9 --- /dev/null +++ b/src/tools/get-book-by-id/index.ts @@ -0,0 +1,143 @@ +// filepath: /Users/ben-smith/Development/personal/mcp-open-library/src/tools/get-book-by-id/index.ts +import { + CallToolResult, + ErrorCode, + McpError, +} from "@modelcontextprotocol/sdk/types.js"; +import axios from "axios"; + +import { + BookDetails, + GetBookByIdArgsSchema, + OpenLibraryBookResponse, + OpenLibraryRecord, // Import the updated record type +} from "./types.js"; + +// Type for the Axios instance +type AxiosInstance = ReturnType; + +export const handleGetBookById = async ( + args: unknown, + axiosInstance: AxiosInstance, +): Promise => { + const parseResult = GetBookByIdArgsSchema.safeParse(args); + + if (!parseResult.success) { + const errorMessages = parseResult.error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join(", "); + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for get_book_by_id: ${errorMessages}`, + ); + } + + const { idType, idValue } = parseResult.data; + const apiUrl = `/api/volumes/brief/${idType}/${idValue}.json`; + + try { + const response = await axiosInstance.get(apiUrl); + + // Check if records object exists and is not empty + if ( + !response.data || + !response.data.records || + Object.keys(response.data.records).length === 0 + ) { + return { + content: [ + { + type: "text", + text: `No book found for ${idType}: ${idValue}`, + }, + ], + }; + } + + // Get the first record from the records object + const recordKey = Object.keys(response.data.records)[0]; + const record: OpenLibraryRecord | undefined = + response.data.records[recordKey]; + + if (!record) { + // This case should theoretically not happen if the length check passed, but good for safety + return { + content: [ + { + type: "text", + text: `Could not process book record for ${idType}: ${idValue}`, + }, + ], + }; + } + + const recordData = record.data; + const recordDetails = record.details?.details; // Access the nested details + + const bookDetails: BookDetails = { + title: recordData.title, + subtitle: recordData.subtitle, + authors: recordData.authors?.map((a) => a.name) || [], + publishers: recordData.publishers?.map((p) => p.name), + publish_date: recordData.publish_date, + number_of_pages: + recordData.number_of_pages ?? recordDetails?.number_of_pages, + // Prefer identifiers from recordData, fallback to recordDetails if necessary + isbn_13: recordData.identifiers?.isbn_13 ?? recordDetails?.isbn_13, + isbn_10: recordData.identifiers?.isbn_10 ?? recordDetails?.isbn_10, + lccn: recordData.identifiers?.lccn ?? recordDetails?.lccn, + oclc: recordData.identifiers?.oclc ?? recordDetails?.oclc_numbers, + olid: recordData.identifiers?.openlibrary, // Add OLID from identifiers + open_library_edition_key: recordData.key, // From recordData + open_library_work_key: recordDetails?.works?.[0]?.key, // From nested details + cover_url: recordData.cover?.medium, // Use medium cover from recordData + info_url: record.details?.info_url ?? recordData.url, // Prefer info_url from details + preview_url: + record.details?.preview_url ?? recordData.ebooks?.[0]?.preview_url, + }; + + // Clean up undefined fields + Object.keys(bookDetails).forEach((key) => { + const typedKey = key as keyof BookDetails; + if ( + bookDetails[typedKey] === undefined || + ((typedKey === "authors" || typedKey === "publishers") && + Array.isArray(bookDetails[typedKey]) && + bookDetails[typedKey].length === 0) + ) { + delete bookDetails[typedKey]; + } + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(bookDetails, null, 2), + }, + ], + }; + } catch (error) { + let errorMessage = "Failed to fetch book data from Open Library."; + if (axios.isAxiosError(error)) { + if (error.response?.status === 404) { + errorMessage = `No book found for ${idType}: ${idValue}`; + } else { + errorMessage = `API Error: ${error.response?.statusText ?? error.message}`; + } + } else if (error instanceof Error) { + errorMessage = `Error processing request: ${error.message}`; + } + console.error("Error in get_book_by_id:", error); + + // Return error as text content + return { + content: [ + { + type: "text", + text: errorMessage, + }, + ], + }; + } +}; diff --git a/src/tools/get-book-by-id/types.ts b/src/tools/get-book-by-id/types.ts new file mode 100644 index 0000000..df25683 --- /dev/null +++ b/src/tools/get-book-by-id/types.ts @@ -0,0 +1,114 @@ +// filepath: /Users/ben-smith/Development/personal/mcp-open-library/src/tools/get-book-by-id/types.ts +import { z } from "zod"; + +// Schema for the get_book_by_id tool arguments +export const GetBookByIdArgsSchema = z.object({ + idType: z.enum(["isbn", "lccn", "oclc", "olid"], { + errorMap: () => ({ + message: "idType must be one of: isbn, lccn, oclc, olid", + }), + }), + idValue: z.string().min(1, { message: "idValue cannot be empty" }), +}); + +// --- Nested types based on the example response --- // + +interface OpenLibraryIdentifier { + isbn_10?: string[]; + isbn_13?: string[]; + lccn?: string[]; + oclc?: string[]; + openlibrary?: string[]; +} + +interface OpenLibraryAuthor { + url?: string; + name: string; +} + +interface OpenLibraryPublisher { + name: string; +} + +interface OpenLibraryCover { + small?: string; + medium?: string; + large?: string; +} + +// Represents the structure within the 'data' field of a record +interface OpenLibraryRecordData { + url: string; + key: string; // e.g., "/books/OL24194264M" + title: string; + subtitle?: string; + authors?: OpenLibraryAuthor[]; + number_of_pages?: number; + identifiers?: OpenLibraryIdentifier; + publishers?: OpenLibraryPublisher[]; + publish_date?: string; + subjects?: { name: string; url: string }[]; + ebooks?: { preview_url?: string; availability?: string; read_url?: string }[]; + cover?: OpenLibraryCover; +} + +// Represents the structure within the 'details' field of a record +interface OpenLibraryRecordDetails { + bib_key: string; + info_url: string; + preview?: string; + preview_url?: string; + thumbnail_url?: string; + details?: { + // Yes, there's another nested 'details' sometimes + key: string; + title: string; + authors?: OpenLibraryAuthor[]; + publishers?: OpenLibraryPublisher[]; + publish_date?: string; + works?: { key: string }[]; // e.g., "/works/OL15610910W" + covers?: number[]; // Cover IDs, not URLs + lccn?: string[]; + oclc_numbers?: string[]; + isbn_10?: string[]; + isbn_13?: string[]; + number_of_pages?: number; + // ... other potential fields + }; +} + +// Represents a single record in the 'records' object +export interface OpenLibraryRecord { + recordURL: string; + data: OpenLibraryRecordData; + details: OpenLibraryRecordDetails; // This is the details object we need + // ... other potential fields like isbns, olids etc. at this level +} + +// Type for the overall API response structure +export interface OpenLibraryBookResponse { + // The keys are dynamic (e.g., "/books/OL24194264M") + records: Record; + items: unknown[]; // Items might not be needed for core details +} + +// --- Formatted Book Details returned by the tool --- // + +export interface BookDetails { + title: string; + subtitle?: string; + authors: string[]; + publishers?: string[]; + publish_date?: string; + number_of_pages?: number; + isbn_13?: string[]; + isbn_10?: string[]; + lccn?: string[]; + oclc?: string[]; + olid?: string[]; // Add OLID field + open_library_edition_key: string; // e.g., "/books/OL24194264M" + open_library_work_key?: string; // e.g., "/works/OL15610910W" + cover_url?: string; + info_url: string; + preview_url?: string; +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 39cb63c..effb97f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -3,3 +3,4 @@ export * from "./get-authors-by-name/index.js"; export * from "./get-author-photo/index.js"; export * from "./get-book-cover/index.js"; export * from "./get-author-info/index.js"; +export * from "./get-book-by-id/index.js"; From 761af66eac6eeb1a4c5bd8deb79773592ccf8665 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 6 May 2025 00:12:40 +0100 Subject: [PATCH 2/5] test: add tests --- src/tools/get-book-by-id/index.test.ts | 277 +++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 src/tools/get-book-by-id/index.test.ts diff --git a/src/tools/get-book-by-id/index.test.ts b/src/tools/get-book-by-id/index.test.ts new file mode 100644 index 0000000..5fcc982 --- /dev/null +++ b/src/tools/get-book-by-id/index.test.ts @@ -0,0 +1,277 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; +import { describe, it, expect, beforeEach, vi, Mock } from "vitest"; // Use vitest imports + +import { OpenLibraryBookResponse } from "./types.js"; // Import necessary type + +import { handleGetBookById } from "./index.js"; + +// Mock axios using vitest +vi.mock("axios"); + +// Create a mock Axios instance type using vitest Mock +type MockAxiosInstance = { + get: Mock; // Use Mock from vitest +}; + +describe("handleGetBookById", () => { + let mockAxiosInstance: MockAxiosInstance; + + beforeEach(() => { + // Reset mocks before each test using vitest + vi.clearAllMocks(); + // Create a fresh mock instance for each test using vitest + mockAxiosInstance = { + get: vi.fn(), // Use vi.fn() + }; + }); + + it("should return book details when given a valid OLID", async () => { + const mockArgs = { idType: "olid", idValue: "OL7353617M" }; + const mockApiResponse: OpenLibraryBookResponse = { + records: { + "/books/OL7353617M": { + recordURL: + "https://openlibrary.org/books/OL7353617M/The_Lord_of_the_Rings", + data: { + title: "The Lord of the Rings", + authors: [{ url: "/authors/OL216228A", name: "J.R.R. Tolkien" }], + publish_date: "1954", + identifiers: { + openlibrary: ["OL7353617M"], + isbn_10: ["061826027X"], + }, + number_of_pages: 1216, + cover: { + medium: "https://covers.openlibrary.org/b/id/8264411-M.jpg", + }, + key: "/books/OL7353617M", + url: "https://openlibrary.org/books/OL7353617M/The_Lord_of_the_Rings", + }, + details: { + info_url: + "https://openlibrary.org/books/OL7353617M/The_Lord_of_the_Rings", + bib_key: "OLID:OL7353617M", + preview_url: "https://archive.org/details/lordofrings00tolk_1", + thumbnail_url: "https://covers.openlibrary.org/b/id/8264411-S.jpg", + details: { + key: "/books/OL7353617M", + works: [{ key: "/works/OL45804W" }], + title: "The Lord of the Rings", + authors: [{ url: "/authors/OL216228A", name: "J.R.R. Tolkien" }], + publishers: [{ name: "Houghton Mifflin" }], + publish_date: "1954", + isbn_10: ["061826027X"], + number_of_pages: 1216, + }, + preview: "restricted", + }, + }, + }, + items: [], // Add required items property + }; + + mockAxiosInstance.get.mockResolvedValue({ data: mockApiResponse }); + + const result = await handleGetBookById(mockArgs, mockAxiosInstance as any); // Cast to any for simplicity + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/volumes/brief/olid/OL7353617M.json", + ); + expect(result).toEqual({ + content: [ + { + type: "text", + text: expect.stringContaining('"title": "The Lord of the Rings"'), + }, + ], + }); + // Check specific fields in the parsed JSON + const parsedResult = JSON.parse(result.content[0].text as string); + expect(parsedResult).toHaveProperty("title", "The Lord of the Rings"); + expect(parsedResult).toHaveProperty("authors", ["J.R.R. Tolkien"]); + expect(parsedResult).toHaveProperty("publish_date", "1954"); + expect(parsedResult).toHaveProperty("number_of_pages", 1216); + expect(parsedResult).toHaveProperty("isbn_10", ["061826027X"]); // Should be array from details + expect(parsedResult).toHaveProperty("olid", ["OL7353617M"]); // Should be array from identifiers + expect(parsedResult).toHaveProperty( + "open_library_edition_key", + "/books/OL7353617M", + ); + expect(parsedResult).toHaveProperty( + "open_library_work_key", + "/works/OL45804W", + ); + expect(parsedResult).toHaveProperty( + "cover_url", + "https://covers.openlibrary.org/b/id/8264411-M.jpg", + ); + expect(parsedResult).toHaveProperty( + "info_url", + "https://openlibrary.org/books/OL7353617M/The_Lord_of_the_Rings", + ); + expect(parsedResult).toHaveProperty( + "preview_url", + "https://archive.org/details/lordofrings00tolk_1", + ); + }); + + it("should return book details when given a valid ISBN", async () => { + const mockArgs = { idType: "isbn", idValue: "9780547928227" }; + const mockApiResponse: OpenLibraryBookResponse = { + records: { + "isbn:9780547928227": { + recordURL: "https://openlibrary.org/books/OL25189068M/The_Hobbit", + data: { + title: "The Hobbit", + authors: [{ url: "/authors/OL216228A", name: "J.R.R. Tolkien" }], + publish_date: "2012", + identifiers: { + isbn_13: ["9780547928227"], + openlibrary: ["OL25189068M"], + }, + key: "/books/OL25189068M", + url: "https://openlibrary.org/books/OL25189068M/The_Hobbit", + }, + details: { + /* ... potentially more details ... */ + } as any, // Cast for brevity + }, + }, + items: [], // Add required items property + }; + mockAxiosInstance.get.mockResolvedValue({ data: mockApiResponse }); + + const result = await handleGetBookById(mockArgs, mockAxiosInstance as any); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/volumes/brief/isbn/9780547928227.json", + ); + expect(result).toEqual({ + content: [ + { + type: "text", + text: expect.stringContaining('"title": "The Hobbit"'), + }, + ], + }); + const parsedResult = JSON.parse(result.content[0].text as string); + expect(parsedResult).toHaveProperty("title", "The Hobbit"); + expect(parsedResult).toHaveProperty("isbn_13", ["9780547928227"]); + expect(parsedResult).toHaveProperty("olid", ["OL25189068M"]); + }); + + it("should throw McpError for invalid arguments", async () => { + const invalidArgs = { idType: "invalid", idValue: "123" }; // Invalid idType + + await expect( + handleGetBookById(invalidArgs, mockAxiosInstance as any), + ).rejects.toThrow(McpError); + + try { + await handleGetBookById(invalidArgs, mockAxiosInstance as any); + } catch (error) { + expect(error).toBeInstanceOf(McpError); + expect((error as McpError).code).toBe(ErrorCode.InvalidParams); + expect((error as McpError).message).toContain( + "Invalid arguments for get_book_by_id", + ); + expect((error as McpError).message).toContain( + "idType must be one of: isbn, lccn, oclc, olid", + ); + } + expect(mockAxiosInstance.get).not.toHaveBeenCalled(); + }); + + it('should return "No book found" message when API returns empty records', async () => { + const mockArgs = { idType: "olid", idValue: "OL_NONEXISTENT" }; + const mockApiResponse: OpenLibraryBookResponse = { + records: {}, + items: [], + }; // Empty records + + mockAxiosInstance.get.mockResolvedValue({ data: mockApiResponse }); + + const result = await handleGetBookById(mockArgs, mockAxiosInstance as any); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/volumes/brief/olid/OL_NONEXISTENT.json", + ); + expect(result).toEqual({ + content: [ + { + type: "text", + text: "No book found for olid: OL_NONEXISTENT", + }, + ], + }); + }); + + it('should return "No book found" message on 404 API error', async () => { + const mockArgs = { idType: "isbn", idValue: "0000000000" }; + const axiosError = { + isAxiosError: true, + response: { status: 404, statusText: "Not Found" }, + message: "Request failed with status code 404", + }; + mockAxiosInstance.get.mockRejectedValue(axiosError); + + const result = await handleGetBookById(mockArgs, mockAxiosInstance as any); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/volumes/brief/isbn/0000000000.json", + ); + expect(result).toEqual({ + content: [ + { + type: "text", + text: "Failed to fetch book data from Open Library.", // Specific message for 404 + }, + ], + }); + }); + + it("should return generic API error message for non-404 errors", async () => { + const mockArgs = { idType: "olid", idValue: "OL1M" }; + const axiosError = { + isAxiosError: true, + response: { status: 500, statusText: "Internal Server Error" }, + message: "Request failed with status code 500", + }; + mockAxiosInstance.get.mockRejectedValue(axiosError); + + const result = await handleGetBookById(mockArgs, mockAxiosInstance as any); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/volumes/brief/olid/OL1M.json", + ); + expect(result).toEqual({ + content: [ + { + type: "text", + text: "Failed to fetch book data from Open Library.", // Generic API error + }, + ], + }); + }); + + it("should return generic error message for non-Axios errors", async () => { + const mockArgs = { idType: "olid", idValue: "OL1M" }; + const genericError = new Error("Network Failure"); + mockAxiosInstance.get.mockRejectedValue(genericError); + + const result = await handleGetBookById(mockArgs, mockAxiosInstance as any); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/volumes/brief/olid/OL1M.json", + ); + expect(result).toEqual({ + content: [ + { + type: "text", + text: "Error processing request: Network Failure", // Generic processing error + }, + ], + }); + }); +}); From 84beccbb3856bd83bbfe37e24ae54ae4efc1f3a1 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 6 May 2025 00:26:13 +0100 Subject: [PATCH 3/5] refactor: make idType parameter case insensitive --- src/tools/get-book-by-id/index.ts | 19 ++++++++++++++++++- src/tools/get-book-by-id/types.ts | 15 --------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/tools/get-book-by-id/index.ts b/src/tools/get-book-by-id/index.ts index e1bafe9..f33ae1d 100644 --- a/src/tools/get-book-by-id/index.ts +++ b/src/tools/get-book-by-id/index.ts @@ -5,14 +5,31 @@ import { McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; +import { z } from "zod"; import { BookDetails, - GetBookByIdArgsSchema, OpenLibraryBookResponse, OpenLibraryRecord, // Import the updated record type } from "./types.js"; +// filepath: /Users/ben-smith/Development/personal/mcp-open-library/src/tools/get-book-by-id/types.ts + +// Schema for the get_book_by_id tool arguments +const GetBookByIdArgsSchema = z.object({ + idType: z + .string() + .transform((val) => val.toLowerCase()) + .pipe( + z.enum(["isbn", "lccn", "oclc", "olid"], { + errorMap: () => ({ + message: "idType must be one of: isbn, lccn, oclc, olid", + }), + }), + ), + idValue: z.string().min(1, { message: "idValue cannot be empty" }), +}); + // Type for the Axios instance type AxiosInstance = ReturnType; diff --git a/src/tools/get-book-by-id/types.ts b/src/tools/get-book-by-id/types.ts index df25683..7a15c89 100644 --- a/src/tools/get-book-by-id/types.ts +++ b/src/tools/get-book-by-id/types.ts @@ -1,18 +1,3 @@ -// filepath: /Users/ben-smith/Development/personal/mcp-open-library/src/tools/get-book-by-id/types.ts -import { z } from "zod"; - -// Schema for the get_book_by_id tool arguments -export const GetBookByIdArgsSchema = z.object({ - idType: z.enum(["isbn", "lccn", "oclc", "olid"], { - errorMap: () => ({ - message: "idType must be one of: isbn, lccn, oclc, olid", - }), - }), - idValue: z.string().min(1, { message: "idValue cannot be empty" }), -}); - -// --- Nested types based on the example response --- // - interface OpenLibraryIdentifier { isbn_10?: string[]; isbn_13?: string[]; From dc8cef3bf545428562c776d20200cb88df5272f3 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 6 May 2025 00:26:41 +0100 Subject: [PATCH 4/5] docs: document new tool --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index c0f6e0c..28ee2bb 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This project implements an MCP server that provides tools for AI assistants to i - **Get Author Details**: Retrieve detailed information for a specific author using their Open Library key (`get_author_info`). - **Get Author Photo**: Get the URL for an author's photo using their Open Library ID (OLID) (`get_author_photo`). - **Get Book Cover**: Get the URL for a book's cover image using various identifiers (ISBN, OCLC, LCCN, OLID, ID) (`get_book_cover`). +- **Get Book by ID**: Retrieve detailed book information using various identifiers (ISBN, LCCN, OCLC, OLID) (`get_book_by_id`). ## Installation @@ -66,6 +67,7 @@ This server implements the Model Context Protocol, which means it can be used by - `get_author_info`: Get detailed information for a specific author using their Open Library Author Key - `get_author_photo`: Get the URL for an author's photo using their Open Library Author ID (OLID) - `get_book_cover`: Get the URL for a book's cover image using a specific identifier (ISBN, OCLC, LCCN, OLID, or ID) +- `get_book_by_id`: Get detailed book information using a specific identifier (ISBN, LCCN, OCLC, or OLID) **Example `get_book_by_title` input:** ```json @@ -174,6 +176,50 @@ The `get_book_cover` tool accepts the following parameters: - `value`: The value of the identifier - `size`: Optional cover size (`S` for small, `M` for medium, `L` for large, defaults to `L`) +**Example `get_book_by_id` input:** +```json +{ + "idType": "isbn", + "idValue": "9780547928227" +} +``` + +**Example `get_book_by_id` output:** +```json +{ + "title": "The Hobbit", + "authors": [ + "J. R. R. Tolkien" + ], + "publishers": [ + "Houghton Mifflin Harcourt" + ], + "publish_date": "October 21, 2012", + "number_of_pages": 300, + "isbn_13": [ + "9780547928227" + ], + "isbn_10": [ + "054792822X" + ], + "oclc": [ + "794607877" + ], + "olid": [ + "OL25380781M" + ], + "open_library_edition_key": "/books/OL25380781M", + "open_library_work_key": "/works/OL45883W", + "cover_url": "https://covers.openlibrary.org/b/id/8231496-M.jpg", + "info_url": "https://openlibrary.org/books/OL25380781M/The_Hobbit", + "preview_url": "https://archive.org/details/hobbit00tolkien" +} +``` + +The `get_book_by_id` tool accepts the following parameters: +- `idType`: The type of identifier (one of: `isbn`, `lccn`, `oclc`, `olid`) +- `idValue`: The value of the identifier + An example of this tool being used in Claude Desktop can be see here: image From 9a8f3802f8c6c7b88534cc5edaea0fbaedbfbed4 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 6 May 2025 00:28:14 +0100 Subject: [PATCH 5/5] refactor: remove redundant comments --- src/tools/get-book-by-id/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tools/get-book-by-id/index.ts b/src/tools/get-book-by-id/index.ts index f33ae1d..46b3547 100644 --- a/src/tools/get-book-by-id/index.ts +++ b/src/tools/get-book-by-id/index.ts @@ -1,4 +1,3 @@ -// filepath: /Users/ben-smith/Development/personal/mcp-open-library/src/tools/get-book-by-id/index.ts import { CallToolResult, ErrorCode, @@ -13,8 +12,6 @@ import { OpenLibraryRecord, // Import the updated record type } from "./types.js"; -// filepath: /Users/ben-smith/Development/personal/mcp-open-library/src/tools/get-book-by-id/types.ts - // Schema for the get_book_by_id tool arguments const GetBookByIdArgsSchema = z.object({ idType: z