Skip to main content
Glama
kujenga
by kujenga

zotero_search_items

Search your Zotero library by query, mode, and tags to find specific items, then retrieve metadata or full text for results.

Instructions

Search for items in your Zotero library, given a query string, query mode (titleCreatorYear or everything), and optional tag search (supports boolean searches). Returned results can be looked up with zotero_item_fulltext or zotero_item_metadata.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYes
qmodeNotitleCreatorYear
tagNo
limitNo

Implementation Reference

  • The @mcp.tool decorator registers the tool and the search_items function implements the core logic: authenticates Zotero client, performs search with parameters, fetches items, and formats results with previews including titles, authors, dates, abstracts, and tags for easy reference.
    @mcp.tool(
        name="zotero_search_items",
        # More detail can be added if useful: https://www.zotero.org/support/dev/web_api/v3/basics#searching
        description="Search for items in your Zotero library, given a query string, query mode (titleCreatorYear or everything), and optional tag search (supports boolean searches). Returned results can be looked up with zotero_item_fulltext or zotero_item_metadata.",
    )
    def search_items(
        query: str,
        qmode: Literal["titleCreatorYear", "everything"] | None = "titleCreatorYear",
        tag: str | None = None,
        limit: int | None = 10,
    ) -> str:
        """Search for items in your Zotero library"""
        zot = get_zotero_client()
    
        # Search using the q parameter
        params = {"q": query, "qmode": qmode, "limit": limit}
        if tag:
            params["tag"] = tag
    
        zot.add_parameters(**params)
        # n.b. types for this return do not work, it's a parsed JSON object
        results: Any = zot.items()
    
        if not results:
            return "No items found matching your query."
    
        # Header with search info
        header = [
            f"# Search Results for: '{query}'",
            f"Found {len(results)} items." + (f" Using tag filter: {tag}" if tag else ""),
            "Use item keys with zotero_item_metadata or zotero_item_fulltext for more details.\n",
        ]
    
        # Format results
        formatted_results = []
        for i, item in enumerate(results):
            data = item["data"]
            item_key = item.get("key", "")
            item_type = data.get("itemType", "unknown")
    
            # Special handling for notes
            if item_type == "note":
                # Get note content
                note_content = data.get("note", "")
                # Strip HTML tags for cleaner text (simple approach)
                note_content = (
                    note_content.replace("<p>", "")
                    .replace("</p>", "\n")
                    .replace("<br>", "\n")
                )
                note_content = note_content.replace("<strong>", "**").replace(
                    "</strong>", "**"
                )
                note_content = note_content.replace("<em>", "*").replace("</em>", "*")
    
                # Extract a title from the first line if possible, otherwise use first few words
                title_preview = ""
                if note_content:
                    lines = note_content.strip().split("\n")
                    first_line = lines[0].strip()
                    if first_line:
                        # Use first line if it's reasonably short, otherwise use first few words
                        if len(first_line) <= 50:
                            title_preview = first_line
                        else:
                            words = first_line.split()
                            title_preview = " ".join(words[:5]) + "..."
    
                # Create a good title for the note
                note_title = title_preview if title_preview else "Note"
    
                # Get a preview of the note content (truncated)
                preview = note_content.strip()
                if len(preview) > 150:
                    preview = preview[:147] + "..."
    
                # Format the note entry
                entry = [
                    f"## {i + 1}. 📝 {note_title}",
                    f"**Type**: Note | **Key**: `{item_key}`",
                    f"\n{preview}",
                ]
    
                # Add parent item reference if available
                if parent_item := data.get("parentItem"):
                    entry.insert(2, f"**Parent Item**: `{parent_item}`")
    
                # Add tags if present (limited to first 5)
                if tags := data.get("tags"):
                    tag_list = [f"`{tag['tag']}`" for tag in tags[:5]]
                    if len(tags) > 5:
                        tag_list.append("...")
                    entry.append(f"\n**Tags**: {' '.join(tag_list)}")
    
                formatted_results.append("\n".join(entry))
                continue
    
            # Regular item processing (non-notes)
            title = data.get("title", "Untitled")
            date = data.get("date", "")
    
            # Format primary creators (limited to first 3)
            creators = []
            for creator in data.get("creators", [])[:3]:
                if "firstName" in creator and "lastName" in creator:
                    creators.append(f"{creator['lastName']}, {creator['firstName']}")
                elif "name" in creator:
                    creators.append(creator["name"])
    
            if len(data.get("creators", [])) > 3:
                creators.append("et al.")
    
            creator_str = "; ".join(creators) if creators else "No authors"
    
            # Get publication or source info
            source = ""
            if pub := data.get("publicationTitle"):
                source = pub
            elif book := data.get("bookTitle"):
                source = f"In: {book}"
            elif publisher := data.get("publisher"):
                source = f"{publisher}"
    
            # Get a brief abstract (truncated if too long)
            abstract = data.get("abstractNote", "")
            if len(abstract) > 150:
                abstract = abstract[:147] + "..."
    
            # Build formatted entry with markdown for better structure
            entry = [
                f"## {i + 1}. {title}",
                f"**Type**: {item_type} | **Date**: {date} | **Key**: `{item_key}`",
                f"**Authors**: {creator_str}",
            ]
    
            if source:
                entry.append(f"**Source**: {source}")
    
            if abstract:
                entry.append(f"\n{abstract}")
    
            # Add tags if present (limited to first 5)
            if tags := data.get("tags"):
                tag_list = [f"`{tag['tag']}`" for tag in tags[:5]]
                if len(tags) > 5:
                    tag_list.append("...")
                entry.append(f"\n**Tags**: {' '.join(tag_list)}")
    
            formatted_results.append("\n".join(entry))
    
        return "\n\n".join(header + formatted_results)
  • Helper function to initialize and return the authenticated Zotero client instance, used by zotero_search_items to perform API calls.
    def get_zotero_client() -> zotero.Zotero:
        """Get authenticated Zotero client using environment variables"""
        library_id = os.getenv("ZOTERO_LIBRARY_ID")
        library_type = os.getenv("ZOTERO_LIBRARY_TYPE", "user")
        api_key = os.getenv("ZOTERO_API_KEY") or None
        local = os.getenv("ZOTERO_LOCAL", "").lower() in ["true", "yes", "1"]
        if local:
            if not library_id:
                # Indicates "current user" for the local API
                library_id = "0"
        elif not all([library_id, api_key]):
            raise ValueError(
                "Missing required environment variables. Please set ZOTERO_LIBRARY_ID and ZOTERO_API_KEY"
            )
    
        return zotero.Zotero(
            library_id=library_id,
            library_type=library_type,
            api_key=api_key,
            local=local,
        )
  • The @mcp.tool decorator registers 'zotero_search_items' with the MCP server, including name and description.
    @mcp.tool(
        name="zotero_search_items",
        # More detail can be added if useful: https://www.zotero.org/support/dev/web_api/v3/basics#searching
        description="Search for items in your Zotero library, given a query string, query mode (titleCreatorYear or everything), and optional tag search (supports boolean searches). Returned results can be looked up with zotero_item_fulltext or zotero_item_metadata.",
    )
  • Input schema defined by function type hints: query (str, required), qmode (Literal or None, default 'titleCreatorYear'), tag (str or None), limit (int or None, default 10), returns str.
    def search_items(
        query: str,
        qmode: Literal["titleCreatorYear", "everything"] | None = "titleCreatorYear",
        tag: str | None = None,
        limit: int | None = 10,
    ) -> str:
Behavior3/5

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

With no annotations provided, the description carries full burden. It describes the search functionality and mentions that results can be looked up with other tools, which adds useful context. However, it doesn't disclose important behavioral traits like whether this is a read-only operation, what permissions are needed, pagination behavior beyond the 'limit' parameter, or error conditions. The description adds some value but leaves significant gaps.

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 efficiently structured in two sentences: the first explains the core functionality with key parameters, the second provides important follow-up context about sibling tools. Every word earns its place with no redundancy or fluff.

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

Completeness3/5

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

For a search tool with 4 parameters, 0% schema coverage, no annotations, and no output schema, the description does a reasonable job explaining the search purpose and parameters. However, it lacks information about return format, error handling, authentication requirements, and doesn't fully document all parameters (missing 'limit'). Given the complexity and lack of structured documentation, this leaves significant gaps.

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

Parameters4/5

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

With 0% schema description coverage, the description must compensate. It explains the purpose of 'query', 'qmode' (with specific mode examples), and 'tag' (including boolean search support). It doesn't mention the 'limit' parameter, but covers 3 of 4 parameters with meaningful context beyond their names. This significantly improves understanding compared to the bare schema.

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 the verb ('Search for items'), resource ('in your Zotero library'), and scope ('given a query string, query mode... and optional tag search'). It distinguishes from siblings by mentioning that results can be looked up with 'zotero_item_fulltext' or 'zotero_item_metadata', indicating this is a search tool while siblings provide detailed item data.

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 for when to use this tool (searching the library with query parameters) and implicitly distinguishes from siblings by noting that results can be looked up with those tools. However, it doesn't explicitly state when NOT to use this tool or provide alternative search methods within the same tool family.

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/kujenga/zotero-mcp'

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