Skip to main content
Glama
freestylefly

WeRead MCP Server

by freestylefly

get_book_notes_and_highlights

Retrieve and organize book highlights and notes by chapter using a specified book ID. Customize results with chapter details, organization, and highlight style filters for structured reading insights.

Instructions

Get all highlights and notes for a specific book, organized by chapter

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
book_idYesBook ID
highlight_styleNoHighlight style filter, null means all
include_chaptersNoWhether to include chapter information
organize_by_chapterNoWhether to organize by chapter

Implementation Reference

  • Main execution logic for the tool: fetches book info, read progress, chapter structure, highlights (via getBookmarkList), notes (via getReviewList), processes and organizes them by chapter if requested, applies filters, formats timestamps, and returns structured JSON response.
    case "get_book_notes_and_highlights": {
      const bookId = String(request.params.arguments?.book_id || "");
      const includeChapters = Boolean(request.params.arguments?.include_chapters !== false);
      const organizeByChapter = Boolean(request.params.arguments?.organize_by_chapter !== false);
      
      // 解析highlight_style参数
      let highlightStyle = null;
      if (request.params.arguments?.highlight_style !== undefined &&
          request.params.arguments?.highlight_style !== null) {
        highlightStyle = Number(request.params.arguments.highlight_style);
      }
      
      if (!bookId) {
        throw new Error("书籍ID不能为空");
      }
      
      // 1. 获取书籍信息
      const bookInfo = await wereadApi.getBookinfo(bookId);
      const bookTitle = bookInfo.title || "";
      
      // 2. 获取书籍阅读进度信息
      const readInfo = await wereadApi.getReadInfo(bookId);
      
      // 3. 获取章节信息
      const chapterInfo = await wereadApi.getChapterInfo(bookId);
      
      // 4. 获取划线数据
      const bookmarkResponse = await wereadApi.getBookmarkList(bookId);
      
      // 确认从响应中获取正确的划线数组
      const highlights = Array.isArray(bookmarkResponse) 
        ? bookmarkResponse 
        : ((bookmarkResponse as any)?.updated || []);
      
      // 5. 获取笔记列表
      const reviews = await wereadApi.getReviewList(bookId);
      
      // 获取开始阅读时间
      const startReadingTime = readInfo.book?.startReadingTime || 0;
      const startReadingTimeISO = startReadingTime > 0 
        ? new Date(startReadingTime * 1000).toISOString() 
        : "";
      
      // 组织数据结构
      const result: any = {
        book_id: bookId,
        book_title: bookTitle,
        book_info: {
          author: bookInfo.author || "",
          translator: bookInfo.translator || "",
          publisher: bookInfo.publisher || "",
          publish_time: bookInfo.publishTime || "",
          word_count: bookInfo.totalWords || 0,
          rating: bookInfo.newRating ? (bookInfo.newRating / 100) : 0,
          category: bookInfo.category || ""
        },
        reading_status: {
          progress: readInfo.book?.progress || 0,
          reading_time: readInfo.book?.readingTime || 0,
          reading_time_formatted: formatReadingTime(readInfo.book?.readingTime || 0),
          start_reading_time: startReadingTimeISO,
          has_started_reading: startReadingTime > 0,
          last_read_time: readInfo.book?.updateTime 
            ? new Date(readInfo.book.updateTime * 1000).toISOString() 
            : "",
          finish_reading: bookInfo.finishReading === 1
        },
        total_highlights: highlights.length,
        total_notes: reviews.length,
        last_updated: new Date().toISOString()
      };
      
      // 处理未分类的内容
      if (organizeByChapter) {
        result.uncategorized = {
          highlights: [],
          notes: []
        };
      } else {
        result.highlights = [];
        result.notes = [];
      }
      
      // 如果需要按章节组织
      if (includeChapters && organizeByChapter) {
        // 第一步:创建所有章节映射 - 从原始数据
        const chapterMap: Record<string, any> = {};
        
        // 将API返回的章节信息转换为我们需要的格式
        const originalChapters = Object.values(chapterInfo);
        
        // 创建基本章节对象 - 简化结构,不保留index和level字段
        originalChapters.forEach((chapter: any) => {
          // 确保chapterUid被转换为字符串
          const chapterUidStr = String(chapter.chapterUid);
          chapterMap[chapterUidStr] = {
            uid: chapter.chapterUid,
            title: chapter.title,
            // 只在构建过程中使用level和index
            _level: chapter.level,
            _index: chapter.chapterIdx,
            children: [],
            highlights: [],
            notes: []
          };
        });
        
        // 第二步:构建章节层级关系
        const rootChapters: any[] = [];
        const chapterLevels: Record<number, any[]> = {};
        
        // 按level分组
        Object.values(chapterMap).forEach(chapter => {
          if (!chapterLevels[chapter._level]) {
            chapterLevels[chapter._level] = [];
          }
          chapterLevels[chapter._level].push(chapter);
        });
        
        // 获取可用的level并排序
        const levels = Object.keys(chapterLevels).map(Number).sort();
        
        // 第一级作为根节点
        if (levels.length > 0) {
          const topLevel = levels[0];
          rootChapters.push(...chapterLevels[topLevel].sort((a, b) => a._index - b._index));
          
          // 从第二级开始,找父章节
          for (let i = 1; i < levels.length; i++) {
            const currentLevel = levels[i];
            
            // 对当前级别的每个章节
            chapterLevels[currentLevel].sort((a, b) => a._index - b._index).forEach(chapter => {
              // 找到前一级别中最近的章节作为父章节
              const prevLevel = levels[i-1];
              const prevLevelChapters = chapterLevels[prevLevel].sort((a, b) => a._index - b._index);
              
              let parent = null;
              for (let j = prevLevelChapters.length - 1; j >= 0; j--) {
                if (prevLevelChapters[j]._index < chapter._index) {
                  parent = prevLevelChapters[j];
                  break;
                }
              }
              
              // 如果找到父章节,添加到其children中
              if (parent) {
                parent.children.push(chapter);
              } else {
                // 如果找不到父章节,直接添加到根
                rootChapters.push(chapter);
              }
            });
          }
        }
        
        // 设置结果
        result.chapters = rootChapters;
        
        // 第三步:处理划线数据 - 根据chapterUid分配到对应章节
        let highlightsAddedCount = 0;
        let uncategorizedCount = 0;
        
        highlights.forEach((highlight: any) => {
          // 确保所有必要的字段都存在
          if (!highlight.markText) {
            return;
          }
          
          const chapterUid = highlight.chapterUid;
          if (!chapterUid) {
            return;
          }
          
          if (highlightStyle !== null && highlight.colorStyle !== highlightStyle) {
            return; // 跳过不匹配的划线样式
          }
          
          const highlightData = {
            text: highlight.markText,
            style: highlight.colorStyle || highlight.style || 0,
            create_time: new Date(highlight.createTime * 1000).toISOString()
          };
          
          // 查找对应章节 - 直接以字符串形式查找
          const chapterUidStr = String(chapterUid);
          const chapter = chapterMap[chapterUidStr];
          
          if (chapter) {
            chapter.highlights.push(highlightData);
            highlightsAddedCount++;
          } else {
            result.uncategorized.highlights.push(highlightData);
            uncategorizedCount++;
          }
        });
        
        // 第四步:处理笔记数据 - 根据chapterUid分配到对应章节
        reviews.forEach((review: any) => {
          // 确保所有必要的字段都存在
          if (!review.content) {
            return;
          }
          
          const chapterUid = review.chapterUid;
          if (!chapterUid) {
            return;
          }
          
          const noteData = {
            content: review.content,
            highlight_text: review.abstract || "",
            create_time: new Date(review.createTime * 1000).toISOString()
          };
          
          // 查找对应章节 - 直接以字符串形式查找
          const chapterUidStr = String(chapterUid);
          const chapter = chapterMap[chapterUidStr];
          
          if (chapter) {
            chapter.notes.push(noteData);
          } else {
            result.uncategorized.notes.push(noteData);
          }
        });
        
        // 第五步:清理不必要的字段并递归移除空章节
        const cleanAndRemoveEmpty = (chapters: any[]): any[] => {
          return chapters.filter(chapter => {
            // 先清理章节对象中用于构建的临时字段
            delete chapter._level;
            delete chapter._index;
            
            // 递归处理子章节
            if (chapter.children && chapter.children.length > 0) {
              chapter.children = cleanAndRemoveEmpty(chapter.children);
            }
            
            // 章节不为空的条件:有划线、有笔记或有非空子章节
            return (
              (chapter.highlights && chapter.highlights.length > 0) ||
              (chapter.notes && chapter.notes.length > 0) ||
              (chapter.children && chapter.children.length > 0)
            );
          });
        };
        
        result.chapters = cleanAndRemoveEmpty(result.chapters);
      } else if (!organizeByChapter) {
        // 非按章节组织模式
        highlights.forEach((highlight: any) => {
          if (!highlight.markText || !highlight.chapterUid) return;
          if (highlightStyle !== null && highlight.colorStyle !== highlightStyle) return;
          
          result.highlights.push({
            text: highlight.markText,
            style: highlight.colorStyle || highlight.style || 0,
            create_time: new Date(highlight.createTime * 1000).toISOString(),
            chapter_uid: highlight.chapterUid,
            chapter_title: chapterInfo[highlight.chapterUid]?.title || "未知章节"
          });
        });
        
        reviews.forEach((review: any) => {
          if (!review.content || !review.chapterUid) return;
          
          result.notes.push({
            content: review.content,
            highlight_text: review.abstract || "",
            create_time: new Date(review.createTime * 1000).toISOString(),
            chapter_uid: review.chapterUid,
            chapter_title: chapterInfo[review.chapterUid]?.title || "未知章节"
          });
        });
      }
      
      return {
        content: [{
          type: "text",
          text: JSON.stringify(result, null, 2)
        }]
      };
    }
  • Input schema definition including required book_id and optional parameters for chapter organization and highlight filtering.
    {
      name: "get_book_notes_and_highlights",
      description: "Get all highlights and notes for a specific book, organized by chapter",
      inputSchema: {
        type: "object",
        properties: {
          book_id: {
            type: "string",
            description: "Book ID"
          },
          include_chapters: {
            type: "boolean",
            description: "Whether to include chapter information",
            default: true
          },
          organize_by_chapter: {
            type: "boolean",
            description: "Whether to organize by chapter",
            default: true
          },
          highlight_style: {
            type: ["integer", "null"],
            description: "Highlight style filter, null means all",
            default: null
          }
        },
        required: ["book_id"]
      }
    },
  • src/index.ts:52-153 (registration)
    Registers the tool by including it in the list returned by ListToolsRequestHandler.
    server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: "get_bookshelf",
            description: "Get all books in the user's bookshelf with comprehensive statistics and categorization information",
            inputSchema: {
              type: "object",
              properties: {},
              required: []
            }
          },
          {
            name: "search_books",
            description: "Search for books in the user's bookshelf by keywords and return matching books with details and reading progress",
            inputSchema: {
              type: "object",
              properties: {
                keyword: {
                  type: "string",
                  description: "Search keyword to match book title, author, translator or category"
                },
                exact_match: {
                  type: "boolean",
                  description: "Whether to use exact matching, default is fuzzy matching",
                  default: false
                },
                include_details: {
                  type: "boolean",
                  description: "Whether to include detailed information",
                  default: true
                },
                max_results: {
                  type: "integer",
                  description: "Maximum number of results to return",
                  default: 5
                }
              },
              required: ["keyword"]
            }
          },
          {
            name: "get_book_notes_and_highlights",
            description: "Get all highlights and notes for a specific book, organized by chapter",
            inputSchema: {
              type: "object",
              properties: {
                book_id: {
                  type: "string",
                  description: "Book ID"
                },
                include_chapters: {
                  type: "boolean",
                  description: "Whether to include chapter information",
                  default: true
                },
                organize_by_chapter: {
                  type: "boolean",
                  description: "Whether to organize by chapter",
                  default: true
                },
                highlight_style: {
                  type: ["integer", "null"],
                  description: "Highlight style filter, null means all",
                  default: null
                }
              },
              required: ["book_id"]
            }
          },
          {
            name: "get_book_best_reviews",
            description: "Get popular reviews for a specific book",
            inputSchema: {
              type: "object",
              properties: {
                book_id: {
                  type: "string",
                  description: "Book ID"
                },
                count: {
                  type: "integer",
                  description: "Number of reviews to return",
                  default: 10
                },
                max_idx: {
                  type: "integer",
                  description: "Pagination index",
                  default: 0
                },
                synckey: {
                  type: "integer",
                  description: "Sync key for pagination",
                  default: 0
                }
              },
              required: ["book_id"]
            }
          },
        ]
      };
    });
  • Helper method to fetch highlights/bookmarks from WeRead API endpoint.
    public async getBookmarkList(bookId: string): Promise<any[]> {
      await this.ensureInitialized();
      return this.retry(async () => {
        const data = await this.makeApiRequest<any>(WEREAD_BOOKMARKLIST_URL, "get", { bookId });
        let bookmarks = data.updated || [];
        // 确保每个划线对象格式一致
        bookmarks = bookmarks.filter((mark: any) => mark.markText && mark.chapterUid);
        return bookmarks;
      });
    }
  • Helper method to fetch notes/reviews from WeRead API endpoint, processes response format.
    public async getReviewList(bookId: string): Promise<any[]> {
      await this.ensureInitialized();
      return this.retry(async () => {
        const data = await this.makeApiRequest<any>(WEREAD_REVIEW_LIST_URL, "get", {
          bookId,
          listType: 4,
          maxIdx: 0,
          count: 0,
          listMode: 2,
          syncKey: 0
        });
        
        let reviews = data.reviews || [];
        // 转换成正确的格式
        reviews = reviews.map((x: any) => x.review);
        
        // 为书评添加chapterUid
        reviews = reviews.map((x: any) => {
          if (x.type === 4) {
            return { chapterUid: 1000000, ...x };
          }
          return x;
        });
        
        return reviews;
      });
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It mentions organization by chapter, which adds some behavioral context, but fails to disclose critical traits such as whether this is a read-only operation, potential rate limits, authentication needs, error conditions, or what the output format looks like (especially since there's no output schema). For a tool with 4 parameters and no annotations, this is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose ('Get all highlights and notes for a specific book') and adds a useful qualifier ('organized by chapter'). There is no wasted verbiage, and every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (4 parameters, no annotations, no output schema), the description is incomplete. It adequately states the purpose but lacks usage guidelines, behavioral transparency (e.g., safety, output format), and doesn't compensate for the absence of an output schema. For a data retrieval tool with multiple parameters, more context is needed to guide effective use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents all parameters. The description adds minimal value beyond the schema by implying organization by chapter (related to 'organize_by_chapter' parameter) but doesn't provide additional syntax, format details, or clarify interactions between parameters (e.g., how 'include_chapters' and 'organize_by_chapter' relate). Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb 'Get' and resource 'highlights and notes for a specific book', with additional context about organization 'by chapter'. It distinguishes from siblings like 'get_book_best_reviews' (reviews vs. notes/highlights) and 'search_books' (searching vs. retrieving specific content). However, it doesn't explicitly differentiate from 'get_bookshelf', which might also involve book-related data retrieval.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like 'get_book_best_reviews' or 'get_bookshelf'. It implies usage for retrieving notes/highlights for a specific book but doesn't specify prerequisites, exclusions, or comparative contexts with sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freestylefly/mcp-server-weread'

If you have feedback or need assistance with the MCP directory API, please join our Discord server