Skip to main content
Glama

search

Find information in your knowledge base by searching across stored Markdown files. This tool helps you locate specific content within your personal semantic graph.

Instructions

Search for content across the knowledge base

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The handler function for the MCP tool named 'search'. This is a ChatGPT/OpenAI compatible adapter that performs a search using the underlying search_notes tool, formats the results in the expected OpenAI schema, and returns them as a content list. Includes registration via @mcp.tool() decorator.
    @mcp.tool(description="Search for content across the knowledge base")
    async def search(
        query: str,
        context: Context | None = None,
    ) -> List[Dict[str, Any]]:
        """ChatGPT/OpenAI MCP search adapter returning a single text content item.
    
        Args:
            query: Search query (full-text syntax supported by `search_notes`)
            context: Optional FastMCP context passed through for auth/session data
    
        Returns:
            List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
            where the JSON body contains `results`, `total_count`, and echo of `query`.
        """
        track_mcp_tool("search")
        logger.info(f"ChatGPT search request: query='{query}'")
    
        try:
            # ChatGPT tools don't expose project parameter, so use default project
            config = ConfigManager().config
            default_project = config.default_project
    
            # Call underlying search_notes with sensible defaults for ChatGPT
            results = await search_notes.fn(
                query=query,
                project=default_project,  # Use default project for ChatGPT
                page=1,
                page_size=10,  # Reasonable default for ChatGPT consumption
                search_type="text",  # Default to full-text search
                context=context,
            )
    
            # Handle string error responses from search_notes
            if isinstance(results, str):
                logger.warning(f"Search failed with error: {results[:100]}...")
                search_results = {
                    "results": [],
                    "error": "Search failed",
                    "error_details": results[:500],  # Truncate long error messages
                }
            else:
                # Format successful results for ChatGPT
                formatted_results = _format_search_results_for_chatgpt(results)
                search_results = {
                    "results": formatted_results,
                    "total_count": len(results.results),  # Use actual count from results
                    "query": query,
                }
                logger.info(f"Search completed: {len(formatted_results)} results returned")
    
            # Return in MCP content array format as required by OpenAI
            return [{"type": "text", "text": json.dumps(search_results, ensure_ascii=False)}]
    
        except Exception as e:
            logger.error(f"ChatGPT search failed for query '{query}': {e}")
            error_results = {
                "results": [],
                "error": "Internal search error",
                "error_message": str(e)[:200],
            }
            return [{"type": "text", "text": json.dumps(error_results, ensure_ascii=False)}]
  • Helper function that formats SearchResponse objects into the list of dictionaries expected by ChatGPT/OpenAI MCP clients (with id, title, url fields).
    def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
        """Format search results according to ChatGPT's expected schema.
    
        Returns a list of result objects with id, title, and url fields.
        """
        formatted_results = []
    
        for result in results.results:
            formatted_result = {
                "id": result.permalink or f"doc-{len(formatted_results)}",
                "title": result.title if result.title and result.title.strip() else "Untitled",
                "url": result.permalink or "",
            }
            formatted_results.append(formatted_result)
    
        return formatted_results
  • Pydantic schemas for search queries and responses (SearchQuery, SearchResult, SearchResponse), used by the underlying search_notes tool invoked by the 'search' MCP tool.
    class SearchQuery(BaseModel):
        """Search query parameters.
    
        Use ONE of these primary search modes:
        - permalink: Exact permalink match
        - permalink_match: Path pattern with *
        - text: Full-text search of title/content (supports boolean operators: AND, OR, NOT)
    
        Optionally filter results by:
        - types: Limit to specific item types
        - entity_types: Limit to specific entity types
        - after_date: Only items after date
    
        Boolean search examples:
        - "python AND flask" - Find items with both terms
        - "python OR django" - Find items with either term
        - "python NOT django" - Find items with python but not django
        - "(python OR flask) AND web" - Use parentheses for grouping
        """
    
        # Primary search modes (use ONE of these)
        permalink: Optional[str] = None  # Exact permalink match
        permalink_match: Optional[str] = None  # Glob permalink match
        text: Optional[str] = None  # Full-text search (now supports boolean operators)
        title: Optional[str] = None  # title only search
    
        # Optional filters
        types: Optional[List[str]] = None  # Filter by type
        entity_types: Optional[List[SearchItemType]] = None  # Filter by entity type
        after_date: Optional[Union[datetime, str]] = None  # Time-based filter
    
        @field_validator("after_date")
        @classmethod
        def validate_date(cls, v: Optional[Union[datetime, str]]) -> Optional[str]:
            """Convert datetime to ISO format if needed."""
            if isinstance(v, datetime):
                return v.isoformat()
            return v
    
        def no_criteria(self) -> bool:
            return (
                self.permalink is None
                and self.permalink_match is None
                and self.title is None
                and self.text is None
                and self.after_date is None
                and self.types is None
                and self.entity_types is None
            )
    
        def has_boolean_operators(self) -> bool:
            """Check if the text query contains boolean operators (AND, OR, NOT)."""
            if not self.text:  # pragma: no cover
                return False
    
            # Check for common boolean operators with correct word boundaries
            # to avoid matching substrings like "GRAND" containing "AND"
            boolean_patterns = [" AND ", " OR ", " NOT ", "(", ")"]
            text = f" {self.text} "  # Add spaces to ensure we match word boundaries
            return any(pattern in text for pattern in boolean_patterns)
    
    
    class SearchResult(BaseModel):
        """Search result with score and metadata."""
    
        title: str
        type: SearchItemType
        score: float
        entity: Optional[Permalink] = None
        permalink: Optional[str]
        content: Optional[str] = None
        file_path: str
    
        metadata: Optional[dict] = None
    
        # IDs for v2 API consistency
        entity_id: Optional[int] = None  # Entity ID (always present for entities)
        observation_id: Optional[int] = None  # Observation ID (for observation results)
        relation_id: Optional[int] = None  # Relation ID (for relation results)
    
        # Type-specific fields
        category: Optional[str] = None  # For observations
        from_entity: Optional[Permalink] = None  # For relations
        to_entity: Optional[Permalink] = None  # For relations
        relation_type: Optional[str] = None  # For relations
    
    
    class SearchResponse(BaseModel):
        """Wrapper for search results."""
    
        results: List[SearchResult]
        current_page: int
  • The underlying 'search_notes' tool that performs the actual search API call and error handling, invoked by the 'search' ChatGPT adapter.
    @mcp.tool(
        description="Search across all content in the knowledge base with advanced syntax support.",
    )
    async def search_notes(
        query: str,
        project: Optional[str] = None,
        page: int = 1,
        page_size: int = 10,
        search_type: str = "text",
        types: List[str] | None = None,
        entity_types: List[str] | None = None,
        after_date: Optional[str] = None,
        context: Context | None = None,
    ) -> SearchResponse | str:
        """Search across all content in the knowledge base with comprehensive syntax support.
    
        This tool searches the knowledge base using full-text search, pattern matching,
        or exact permalink lookup. It supports filtering by content type, entity type,
        and date, with advanced boolean and phrase search capabilities.
    
        Project Resolution:
        Server resolves projects in this order: Single Project Mode → project parameter → default project.
        If project unknown, use list_memory_projects() or recent_activity() first.
    
        ## Search Syntax Examples
    
        ### Basic Searches
        - `search_notes("my-project", "keyword")` - Find any content containing "keyword"
        - `search_notes("work-docs", "'exact phrase'")` - Search for exact phrase match
    
        ### Advanced Boolean Searches
        - `search_notes("my-project", "term1 term2")` - Find content with both terms (implicit AND)
        - `search_notes("my-project", "term1 AND term2")` - Explicit AND search (both terms required)
        - `search_notes("my-project", "term1 OR term2")` - Either term can be present
        - `search_notes("my-project", "term1 NOT term2")` - Include term1 but exclude term2
        - `search_notes("my-project", "(project OR planning) AND notes")` - Grouped boolean logic
    
        ### Content-Specific Searches
        - `search_notes("research", "tag:example")` - Search within specific tags (if supported by content)
        - `search_notes("work-project", "category:observation")` - Filter by observation categories
        - `search_notes("team-docs", "author:username")` - Find content by author (if metadata available)
    
        ### Search Type Examples
        - `search_notes("my-project", "Meeting", search_type="title")` - Search only in titles
        - `search_notes("work-docs", "docs/meeting-*", search_type="permalink")` - Pattern match permalinks
        - `search_notes("research", "keyword", search_type="text")` - Full-text search (default)
    
        ### Filtering Options
        - `search_notes("my-project", "query", types=["entity"])` - Search only entities
        - `search_notes("work-docs", "query", types=["note", "person"])` - Multiple content types
        - `search_notes("research", "query", entity_types=["observation"])` - Filter by entity type
        - `search_notes("team-docs", "query", after_date="2024-01-01")` - Recent content only
        - `search_notes("my-project", "query", after_date="1 week")` - Relative date filtering
    
        ### Advanced Pattern Examples
        - `search_notes("work-project", "project AND (meeting OR discussion)")` - Complex boolean logic
        - `search_notes("research", "\"exact phrase\" AND keyword")` - Combine phrase and keyword search
        - `search_notes("dev-notes", "bug NOT fixed")` - Exclude resolved issues
        - `search_notes("archive", "docs/2024-*", search_type="permalink")` - Year-based permalink search
    
        Args:
            query: The search query string (supports boolean operators, phrases, patterns)
            project: Project name to search in. Optional - server will resolve using hierarchy.
                    If unknown, use list_memory_projects() to discover available projects.
            page: The page number of results to return (default 1)
            page_size: The number of results to return per page (default 10)
            search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
            types: Optional list of note types to search (e.g., ["note", "person"])
            entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
            after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
            context: Optional FastMCP context for performance caching.
    
        Returns:
            SearchResponse with results and pagination info, or helpful error guidance if search fails
    
        Examples:
            # Basic text search
            results = await search_notes("project planning")
    
            # Boolean AND search (both terms must be present)
            results = await search_notes("project AND planning")
    
            # Boolean OR search (either term can be present)
            results = await search_notes("project OR meeting")
    
            # Boolean NOT search (exclude terms)
            results = await search_notes("project NOT meeting")
    
            # Boolean search with grouping
            results = await search_notes("(project OR planning) AND notes")
    
            # Exact phrase search
            results = await search_notes("\"weekly standup meeting\"")
    
            # Search with type filter
            results = await search_notes(
                "meeting notes",
                types=["entity"],
            )
    
            # Search with entity type filter
            results = await search_notes(
                "meeting notes",
                entity_types=["observation"],
            )
    
            # Search for recent content
            results = await search_notes(
                "bug report",
                after_date="1 week"
            )
    
            # Pattern matching on permalinks
            results = await search_notes(
                "docs/meeting-*",
                search_type="permalink"
            )
    
            # Title-only search
            results = await search_notes(
                "Machine Learning",
                search_type="title"
            )
    
            # Complex search with multiple filters
            results = await search_notes(
                "(bug OR issue) AND NOT resolved",
                types=["entity"],
                after_date="2024-01-01"
            )
    
            # Explicit project specification
            results = await search_notes("project planning", project="my-project")
        """
        track_mcp_tool("search_notes")
        # Avoid mutable-default-argument footguns. Treat None as "no filter".
        types = types or []
        entity_types = entity_types or []
    
        # Create a SearchQuery object based on the parameters
        search_query = SearchQuery()
    
        # Set the appropriate search field based on search_type
        if search_type == "text":
            search_query.text = query
        elif search_type == "title":
            search_query.title = query
        elif search_type == "permalink" and "*" in query:
            search_query.permalink_match = query
        elif search_type == "permalink":
            search_query.permalink = query
        else:
            search_query.text = query  # Default to text search
    
        # Add optional filters if provided (empty lists are treated as no filter)
        if entity_types:
            search_query.entity_types = [SearchItemType(t) for t in entity_types]
        if types:
            search_query.types = types
        if after_date:
            search_query.after_date = after_date
    
        async with get_client() as client:
            active_project = await get_active_project(client, project, context)
    
            logger.info(f"Searching for {search_query} in project {active_project.name}")
    
            try:
                response = await call_post(
                    client,
                    f"/v2/projects/{active_project.external_id}/search/",
                    json=search_query.model_dump(),
                    params={"page": page, "page_size": page_size},
                )
                result = SearchResponse.model_validate(response.json())
    
                # Check if we got no results and provide helpful guidance
                if not result.results:
                    logger.info(
                        f"Search returned no results for query: {query} in project {active_project.name}"
                    )
                    # Don't treat this as an error, but the user might want guidance
                    # We return the empty result as normal - the user can decide if they need help
    
                return result
    
            except Exception as e:
                logger.error(f"Search failed for query '{query}': {e}, project: {active_project.name}")
                # Return formatted error message as string for better user experience
                return _format_search_error_response(active_project.name, str(e), query, search_type)
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 of behavioral disclosure. It only states the action ('Search') without any details on permissions, rate limits, result format, pagination, or error handling. For a search tool with no annotation coverage, this leaves critical behavioral traits unspecified.

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 a single, efficient sentence with no wasted words. It's front-loaded and to the point, though it could benefit from more detail given the tool's complexity. The structure is clear but overly brief for a tool with no annotations and low schema coverage.

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?

Given the tool's moderate complexity (search operation), no annotations, low schema coverage (0%), but presence of an output schema, the description is minimally complete. It states the basic purpose but lacks details on behavior, parameters, and usage context. The output schema helps, but the description doesn't fully compensate for the gaps in annotations and schema.

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?

The input schema has 1 parameter with 0% description coverage, so the description must compensate. It mentions 'query' implicitly but doesn't explain what the query parameter expects (e.g., keywords, filters, syntax). Since schema coverage is low, the description adds minimal value beyond the schema, meeting the baseline for adequate but incomplete coverage.

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

Purpose3/5

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

The description 'Search for content across the knowledge base' states a clear verb ('Search') and resource ('content across the knowledge base'), but it's vague about what 'content' specifically means and doesn't distinguish this tool from sibling tools like 'search_notes' or 'fetch'. It provides a basic purpose but lacks specificity.

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 offers no guidance on when to use this tool versus alternatives. With siblings like 'search_notes', 'fetch', and 'read_content' available, there's no indication of what makes this search tool unique or when it's preferred over other search or retrieval tools. It's a generic statement with no usage context.

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/basicmachines-co/basic-memory'

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