Skip to main content
Glama

reader_list_documents

Read-onlyIdempotent

List documents from your Readwise Reader library filtered by ID, location, category, tag, or update time. Retrieve specific documents, paginate results, or include full HTML content. Organize your reading list and knowledge repository.

Instructions

List documents from your Readwise Reader library.

Args:
    id: Get a single document by ID (returns only that document)
    location: Filter by folder location (new, later, shortlist, archive, feed)
    category: Filter by category (article, email, rss, highlight, note, pdf, epub, tweet, video)
    tag: Filter by tag keys (pass empty list [] for untagged documents)
    updatedAfter: Filter by update time (ISO 8601 format, e.g., 2024-01-01T00:00:00Z)
    limit: Number of results to return (1-100, default: 100)
    withContent: Include full HTML content in response (default: false)
    pageCursor: Pagination cursor for next page

Returns:
    ListDocumentResponse with count, results, and nextPageCursor

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
idNo
locationNo
categoryNo
tagNo
updatedAfterNo
limitNo
withContentNo
pageCursorNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
countYes
resultsYes
nextPageCursorNo

Implementation Reference

  • main.py:165-274 (handler)
    The main handler function for the 'reader_list_documents' MCP tool. It validates inputs (location, category, ISO 8601 datetime, limit), builds query parameters, calls the Readwise Reader API /list/ endpoint, parses the response into ReaderDocument objects, and returns a ListDocumentResponse.
    async def reader_list_documents(
        id: Optional[str] = None,
        location: Optional[Literal["new", "later", "shortlist", "archive", "feed"]] = None,
        category: Optional[Literal["article", "email", "rss", "highlight", "note", "pdf", "epub", "tweet", "video"]] = None,
        tag: Optional[List[str]] = None,
        updatedAfter: Optional[str] = None,
        limit: Optional[int] = None,
        withContent: Optional[bool] = False,
        pageCursor: Optional[str] = None,
    ) -> ListDocumentResponse:
        """
        List documents from your Readwise Reader library.
    
        Args:
            id: Get a single document by ID (returns only that document)
            location: Filter by folder location (new, later, shortlist, archive, feed)
            category: Filter by category (article, email, rss, highlight, note, pdf, epub, tweet, video)
            tag: Filter by tag keys (pass empty list [] for untagged documents)
            updatedAfter: Filter by update time (ISO 8601 format, e.g., 2024-01-01T00:00:00Z)
            limit: Number of results to return (1-100, default: 100)
            withContent: Include full HTML content in response (default: false)
            pageCursor: Pagination cursor for next page
    
        Returns:
            ListDocumentResponse with count, results, and nextPageCursor
        """
        ctx = get_reader_context()
        logger.info(
            f"reader_list_documents: id={id}, location={location}, category={category}, "
            f"tag={tag}, updatedAfter={updatedAfter}, limit={limit}, withContent={withContent}, "
            f"pageCursor={pageCursor}"
        )
    
        try:
            # Validate parameters
            if location:
                _validate_location(location)
            if category:
                _validate_category(category)
            if updatedAfter and not _validate_iso8601_datetime(updatedAfter):
                logger.warning(
                    f"Invalid ISO 8601 datetime: {updatedAfter}. "
                    "Expected format: YYYY-MM-DDTHH:MM:SSZ"
                )
                updatedAfter = None
            if limit is not None and (limit < 1 or limit > 100):
                raise ValueError(f"Limit must be between 1 and 100. Got: {limit}")
    
            # Build query params
            params: Dict[str, Any] = {}
            if id:
                params["id"] = id
            if location:
                params["location"] = location
            if category:
                params["category"] = category
            if tag is not None:
                # Pass empty string for untagged, or multiple tag values
                if len(tag) == 0:
                    params["tag"] = ""
                else:
                    params["tag"] = tag
            if updatedAfter:
                params["updatedAfter"] = updatedAfter
            if limit:
                params["limit"] = limit
            if withContent:
                params["withHtmlContent"] = withContent
            if pageCursor:
                params["pageCursor"] = pageCursor
    
            # Make API request
            response = await ctx.client.get("/list/", params=params)
            response.raise_for_status()
            data = response.json()
    
            # Parse response
            try:
                results = [
                    ReaderDocument.from_dict(doc) for doc in data.get("results", [])
                ]
                list_response = ListDocumentResponse(
                    count=data.get("count", 0),
                    results=results,
                    nextPageCursor=data.get("nextPageCursor"),
                )
            except (TypeError, ValueError) as e:
                logger.error(f"Invalid API response format: {e}")
                raise ValueError(
                    "Unexpected response format from Reader API. "
                    "Expected 'count', 'results' fields."
                )
    
            # Return ListDocumentResponse
            return list_response
    
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise ValueError(
                    "Authentication failed. Please check your READWISE_ACCESS_TOKEN. "
                    "Get your token from: https://readwise.io/access_token"
                )
            elif e.response.status_code == 429:
                raise _rate_limit_error(e.response)
            raise
        except ValueError as e:
            raise
        except Exception as e:
            logger.error(f"Error in reader_list_documents: {str(e)}")
            raise ValueError(f"Failed to list documents: {str(e)}")
  • main.py:156-163 (registration)
    Registers the 'reader_list_documents' tool with the MCP server using @mcp.tool decorator, including tool annotations (readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=True).
    @mcp.tool(
        name="reader_list_documents",
        annotations=ToolAnnotations(
            readOnlyHint=True,
            destructiveHint=False,
            idempotentHint=True,
            openWorldHint=True,
        ),
  • ReaderDocument dataclass representing a document object from the Reader API. Used to parse individual results from the list endpoint.
    @dataclass
    class ReaderDocument:
        """
        Document object in Reader.
        """
        # Required document identifiers
        id: str
        url: str
    
        # Document details
        title: str
        source_url: Optional[str] = None
        author: Optional[str] = None
        source: Optional[str] = None
        category: Optional[str] = None
        location: Optional[str] = None
        tags: Dict[str, Any] = field(default_factory=dict)
        site_name: Optional[str] = None
        word_count: Optional[int] = None
        notes: Optional[str] = None
        published_date: Optional[str] = None
        summary: Optional[str] = None
        html_content: Optional[str] = None
        image_url: Optional[str] = None
        parent_id: Optional[str] = None
    
        # Reading state
        reading_progress: float = 0.0
        first_opened_at: Optional[datetime] = None
        last_opened_at: Optional[datetime] = None
    
        # Timestamps
        created_at: Optional[datetime] = None
        updated_at: Optional[datetime] = None
        saved_at: Optional[datetime] = None
        last_moved_at: Optional[datetime] = None
    
        @classmethod
        def from_dict(cls, doc: Dict[str, Any]) -> "ReaderDocument":
            """Create a ReaderDocument from a dictionary representation"""
            return cls(
                id=doc.get('id', ''),
                url=doc.get('url', ''),
                title=doc.get('title', 'Untitled'),
                source_url=doc.get('source_url'),
                author=doc.get('author'),
                source=doc.get('source'),
                category=doc.get('category'),
                location=doc.get('location'),
                tags=doc.get('tags', {}),
                site_name=doc.get('site_name'),
                word_count=doc.get('word_count'),
                notes=doc.get('notes'),
                published_date=doc.get('published_date'),
                summary=doc.get('summary'),
                html_content=doc.get('html_content'),
                image_url=doc.get('image_url'),
                parent_id=doc.get('parent_id'),
                reading_progress=doc.get('reading_progress', 0.0),
                first_opened_at=doc.get('first_opened_at'),
                last_opened_at=doc.get('last_opened_at'),
                created_at=doc.get('created_at'),
                updated_at=doc.get('updated_at'),
                saved_at=doc.get('saved_at'),
                last_moved_at=doc.get('last_moved_at')
            )
    
    
    @dataclass
    class ListDocumentResponse:
        """Response of the document list API"""
        count: int
        results: List[ReaderDocument]
        nextPageCursor: Optional[str] = None
  • ListDocumentResponse dataclass representing the response of the document list API. Contains count, results (list of ReaderDocument), and optional nextPageCursor.
    @dataclass
    class ListDocumentResponse:
        """Response of the document list API"""
        count: int
        results: List[ReaderDocument]
        nextPageCursor: Optional[str] = None
  • Helper function that validates the location parameter against a set of valid locations.
    def _validate_location(location: str, valid_set: set = VALID_LOCATIONS) -> None:
        """Validate location parameter."""
        if location not in valid_set:
            raise ValueError(
                f"Invalid location '{location}'. "
                f"Must be one of: {', '.join(sorted(valid_set))}"
            )
Behavior4/5

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

Annotations already indicate the tool is read-only (readOnlyHint=true) and idempotent. The description adds behavioral details such as pagination via pageCursor, filtering options, and the withContent flag affecting response size. There is no contradiction with annotations.

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

Conciseness4/5

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

The description is well-structured with a clear introductory sentence followed by an 'Args:' block listing each parameter. It is front-loaded with the main purpose. While not overly verbose, the parameter descriptions could be slightly more concise (e.g., combine similar filters), but overall it is efficient.

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

Completeness5/5

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

Given the tool has 8 parameters, 0 required, and an output schema described, the description covers all necessary aspects: purpose, all parameters with semantics, and return type (ListDocumentResponse with count, results, nextPageCursor). It is complete for a listing tool with no missing details.

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

Parameters5/5

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

The input schema has 0% description coverage, so the tool description must compensate. It provides brief but clear semantics for all 8 parameters, including the meaning of 'tag' (filter by tag keys, empty list for untagged) and 'updatedAfter' (ISO 8601 format). This adds essential meaning beyond the schema's type and enum definitions.

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

Purpose5/5

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

The description clearly states 'List documents from your Readwise Reader library', specifying the verb (list) and resource (documents). It distinguishes from sibling tools like reader_create_document and reader_delete_document by focusing on listing and filtering.

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

Usage Guidelines4/5

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

The description provides clear context on when to use the tool (to list documents) and explains each parameter's filtering purpose, including special cases like empty tag list for untagged documents. However, it does not explicitly state when not to use this tool compared to alternatives, though the sibling tools are distinct operations.

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

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/xinthink/reader-mcp-server'

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