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:
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.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
+ },
+ ],
+ });
+ });
+});
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..46b3547
--- /dev/null
+++ b/src/tools/get-book-by-id/index.ts
@@ -0,0 +1,157 @@
+import {
+ CallToolResult,
+ ErrorCode,
+ McpError,
+} from "@modelcontextprotocol/sdk/types.js";
+import axios from "axios";
+import { z } from "zod";
+
+import {
+ BookDetails,
+ OpenLibraryBookResponse,
+ OpenLibraryRecord, // Import the updated record type
+} from "./types.js";
+
+// 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;
+
+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..7a15c89
--- /dev/null
+++ b/src/tools/get-book-by-id/types.ts
@@ -0,0 +1,99 @@
+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";