Skip to main content
Glama
cdmx-in
by cdmx-in

search_project_documents

Search for documents within a specific Goodday project by name or content. Filter results with optional document name matching and include full document content when needed.

Instructions

Search for documents in a specific project.

Args: project_name: The name of the project to search in (case-insensitive) document_name: Optional document name to filter by (case-insensitive partial match) include_content: Whether to include the full content of each document

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_nameYes
document_nameNo
include_contentNo

Implementation Reference

  • The handler function implementing the 'search_project_documents' MCP tool. It locates the project, retrieves and filters documents, optionally fetches and includes content, and returns formatted results.
    async def search_project_documents(project_name: str, document_name: Optional[str] = None, include_content: bool = False) -> str:
        """Search for documents in a specific project.
    
        Args:
            project_name: The name of the project to search in (case-insensitive)
            document_name: Optional document name to filter by (case-insensitive partial match)
            include_content: Whether to include the full content of each document
        """
        # Find project
        projects_data = await make_goodday_request("projects?archived=true")
        if not projects_data or not isinstance(projects_data, list):
            return "Unable to fetch projects."
    
        project_type_projects = [p for p in projects_data if isinstance(p, dict) and p.get('systemType') in ['PROJECT', 'FOLDER']]
        matching_projects = []
        search_term = project_name.lower()
        for project in project_type_projects:
            if isinstance(project, dict) and search_term in project.get('name', '').lower():
                matching_projects.append(project)
    
        if not matching_projects:
            return f"No projects found containing '{project_name}' in their name."
    
        target_project = matching_projects[0]
        actual_project_name = target_project.get('name')
        project_id = target_project.get('id')
    
        # Get documents
        documents_data = await make_goodday_request(f"project/{project_id}/documents")
        if not documents_data:
            return f"No documents found in project '{actual_project_name}'."
    
        if isinstance(documents_data, dict) and "error" in documents_data:
            return f"Unable to fetch documents: {documents_data.get('error', 'Unknown error')}"
    
        if not isinstance(documents_data, list):
            return f"Unexpected response format for documents: {str(documents_data)}"
    
        # Filter by document name if specified
        if document_name:
            filtered_documents = []
            for doc in documents_data:
                if isinstance(doc, dict) and document_name.lower() in doc.get('name', '').lower():
                    filtered_documents.append(doc)
            documents_data = filtered_documents
    
        if not documents_data:
            return f"No documents found matching '{document_name}' in project '{actual_project_name}'."
    
        # Get mappings
        user_id_to_name = await get_user_mapping()
        project_id_to_name = await get_project_mapping()
    
        # Format documents
        formatted_docs = []
        for doc in documents_data:
            if isinstance(doc, dict):
                doc_id = doc.get('id', 'N/A')
                doc_content = ""
                
                if include_content and doc_id != 'N/A':
                    try:
                        content_data = await make_goodday_request(f"document/{doc_id}")
                        if content_data:
                            if isinstance(content_data, dict):
                                doc_content = content_data.get('content', content_data.get('text', str(content_data)))
                            else:
                                doc_content = str(content_data)
                    except Exception as e:
                        doc_content = f"Error fetching content: {str(e)}"
                
                project_id_val = doc.get('projectId', 'N/A')
                project_name_val = project_id_to_name.get(project_id_val, f"Project {project_id_val}") if project_id_val != 'N/A' else 'N/A'
                
                created_by_id = doc.get('createdByUserId', 'N/A')
                created_by_name = user_id_to_name.get(created_by_id, f"User {created_by_id}") if created_by_id != 'N/A' else 'N/A'
                
                formatted_doc = f"""
    **Document ID:** {doc_id}
    **Name:** {doc.get('name', 'N/A')}
    **Project:** {project_name_val}
    **Created By:** {created_by_name}
    **Created:** {format_timestamp_ist(doc.get('momentCreated', 'N/A'))}
    **Updated:** {format_timestamp_ist(doc.get('momentUpdated', 'N/A'))}"""
                
                if include_content:
                    formatted_doc += f"\n**Content:**\n{doc_content}"
                
                formatted_docs.append(formatted_doc.strip())
    
        result = "\n---\n".join(formatted_docs)
        filter_text = f" matching '{document_name}'" if document_name else ""
        return f"**Documents in project '{actual_project_name}'{filter_text}:**\n\n{result}"
  • The @mcp.tool() decorator registers the search_project_documents function as an MCP tool.
    async def search_project_documents(project_name: str, document_name: Optional[str] = None, include_content: bool = False) -> str:
  • Helper function used by search_project_documents to make API requests to Goodday.
    async def make_goodday_request(endpoint: str, method: str = "GET", data: dict = None, subfolders: bool = True) -> dict[str, Any] | list[Any] | None:
        """Make a request to the Goodday API with proper error handling."""
        api_token = os.getenv("GOODDAY_API_TOKEN")
        if not api_token:
            raise ValueError("GOODDAY_API_TOKEN environment variable is required")
        
        headers = {
            "User-Agent": USER_AGENT,
            "gd-api-token": api_token,
            "Content-Type": "application/json"
        }
        
        # Automatically add subfolders=true for project task and document endpoints if not already present
        if subfolders and endpoint.startswith("project/") and ("/tasks" in endpoint or "/documents" in endpoint):
            if "?" in endpoint:
                if "subfolders=" not in endpoint:
                    endpoint += "&subfolders=true"
            else:
                endpoint += "?subfolders=true"
        
        url = f"{GOODDAY_API_BASE}/{endpoint.lstrip('/')}"
        
        async with httpx.AsyncClient() as client:
            try:
                if method.upper() == "POST":
                    response = await client.post(url, headers=headers, json=data, timeout=30.0)
                elif method.upper() == "PUT":
                    response = await client.put(url, headers=headers, json=data, timeout=30.0)
                elif method.upper() == "DELETE":
                    response = await client.delete(url, headers=headers, timeout=30.0)
                else:
                    response = await client.get(url, headers=headers, timeout=30.0)
    
                response.raise_for_status()
                return response.json()
    
            except httpx.HTTPStatusError as e:
                raise Exception(f"HTTP error {e.response.status_code}: {e.response.text}")
            except httpx.RequestError as e:
                raise Exception(f"Request error: {str(e)}")
            except Exception as e:
                raise Exception(f"Unexpected error: {str(e)}")
  • Helper function providing project ID to name mapping, used in formatting documents.
    """Get mapping of project IDs to names."""
    data = await make_goodday_request("projects")
    project_id_to_name = {}
    if isinstance(data, list):
        for p in data:
            if isinstance(p, dict):
                project_id_to_name[p.get("id")] = p.get("name", "Unknown")
    return project_id_to_name
  • Helper function providing user ID to name mapping, used in formatting documents.
    """Get mapping of user IDs to names."""
    data = await make_goodday_request("users")
    user_id_to_name = {}
    if isinstance(data, list):
        for u in data:
            if isinstance(u, dict):
                user_id_to_name[u.get("id")] = u.get("name", "Unknown")
    return user_id_to_name
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 mentions that searches are 'case-insensitive' and 'partial match' for document_name, which adds useful context beyond the schema. However, it doesn't cover critical behaviors like pagination, rate limits, authentication needs, error conditions, or what the output looks like (e.g., list format). For a search tool with zero annotation coverage, 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.

Conciseness4/5

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

The description is appropriately sized and front-loaded: the first sentence states the purpose clearly, followed by a structured 'Args:' section with concise explanations for each parameter. There's no wasted text, and the information is organized for quick scanning. It could be slightly more concise by integrating the parameter details into the main flow, but it's efficient overall.

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 complexity (a search tool with 3 parameters), no annotations, and no output schema, the description is partially complete. It covers parameter semantics well but lacks details on behavioral aspects like output format, error handling, or performance characteristics. For a tool with no structured output or annotation support, the description should do more to compensate, but it meets a minimum viable level by explaining the parameters and basic search behavior.

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?

Schema description coverage is 0%, so the description must compensate. It adds meaningful semantics for all three parameters: 'project_name' is required and case-insensitive, 'document_name' is optional with case-insensitive partial matching, and 'include_content' controls whether full content is returned. This goes beyond the schema's basic type definitions, providing practical usage details that help the agent invoke the tool correctly.

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 tool's purpose: 'Search for documents in a specific project.' It specifies the verb ('search'), resource ('documents'), and scope ('in a specific project'), which is clear and specific. However, it doesn't explicitly differentiate from sibling tools like 'get_document_content' or 'search_goodday_tasks', which would require a 5.

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. There's no mention of sibling tools like 'get_document_content' (for retrieving content of a specific document) or 'search_goodday_tasks' (for searching tasks instead of documents), nor any context about prerequisites or exclusions. The only implied usage is searching documents within projects, but this is basic and lacks explicit alternatives.

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/cdmx-in/goodday-mcp'

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