list_documents
Retrieve documents from Frappe with custom filters, field selection, and sorting to efficiently query and manage data.
Instructions
List documents from Frappe with filters.
Args:
doctype: DocType name
filters: Filter string (optional). Uses custom syntax to bypass MCP validation issues.
fields: Comma-separated field names (optional). E.g. "name,customer,total"
limit: Maximum number of records to return (optional). E.g. "20"
order_by: Field to order by (optional, can include 'desc' like 'creation desc')
Filter Syntax:
- Simple equality: "field:value" -> {"field": "value"}
- Operators: "field:operator:value" -> {"field": ["operator", value]}
- Multiple filters: "field1:value1,field2:operator:value2"
Supported Operators:
- Equality: = (default), !=
- Comparison: <, >, <=, >=
- Pattern: like, not_like (use % for wildcards)
- Lists: in, not_in (separate values with |)
- Null checks: is:null, is:not_null, is_not:null
- Ranges: between (separate values with |)
Examples:
- list_documents("Bank Transaction", "status:Unreconciled") -> List unreconciled transactions
- list_documents("Task", "status:in:Open|Working", "name,subject", "10") -> List open tasks with specific fields
- list_documents("User", "name:like:%admin%") -> List users with 'admin' in name
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| doctype | Yes | ||
| fields | No | ||
| filters | No | ||
| limit | No | ||
| order_by | No |
Implementation Reference
- src/tools/documents.py:350-416 (handler)The primary handler implementation for the 'list_documents' tool. It accepts doctype and optional filter, fields, limit, order_by parameters, parses filters using format_filters_for_api, constructs Frappe REST API query parameters, fetches the document list, and returns formatted JSON results with count.@mcp.tool() async def list_documents( doctype: str, filters: Optional[str] = None, fields: Optional[str] = None, limit: Optional[str] = None, order_by: Optional[str] = None ) -> str: """ List documents from Frappe with filters. Args: doctype: DocType name filters: Filter string (optional). Uses custom syntax to bypass MCP validation issues. fields: Comma-separated field names (optional). E.g. "name,customer,total" limit: Maximum number of records to return (optional). E.g. "20" order_by: Field to order by (optional, can include 'desc' like 'creation desc') Filter Syntax: - Simple equality: "field:value" -> {"field": "value"} - Operators: "field:operator:value" -> {"field": ["operator", value]} - Multiple filters: "field1:value1,field2:operator:value2" Supported Operators: - Equality: = (default), != - Comparison: <, >, <=, >= - Pattern: like, not_like (use % for wildcards) - Lists: in, not_in (separate values with |) - Null checks: is:null, is:not_null, is_not:null - Ranges: between (separate values with |) Examples: - list_documents("Bank Transaction", "status:Unreconciled") -> List unreconciled transactions - list_documents("Task", "status:in:Open|Working", "name,subject", "10") -> List open tasks with specific fields - list_documents("User", "name:like:%admin%") -> List users with 'admin' in name """ try: client = get_client() # Build query parameters params = {} parsed_filters = format_filters_for_api(filters) if parsed_filters: params["filters"] = json.dumps(parsed_filters) if fields: # Convert comma-separated string to list field_list = [f.strip() for f in fields.split(',')] params["fields"] = json.dumps(field_list) if limit: # Convert string to integer for API params["limit"] = limit if order_by: params["order_by"] = order_by # Make API request to list documents response = await client.get(f"api/resource/{doctype}", params=params) if "data" in response: documents = response["data"] count = len(documents) return f"Found {count} {doctype} documents:\n\n" + json.dumps(documents, indent=2) else: return json.dumps(response, indent=2) except Exception as error: return _format_error_response(error, "list_documents")
- src/server.py:40-40 (registration)Registration of the documents tools module in the main MCP server setup, which includes the list_documents tool via its register_tools function.documents.register_tools(mcp)
- src/tools/filter_parser.py:11-108 (helper)Key helper function parse_filter_string that parses the custom string-based filter syntax (e.g., 'status:Unreconciled', 'amount:>:100') into Frappe-compatible filter dictionaries. Called via format_filters_for_api in the list_documents handler.def parse_filter_string(filter_str: str) -> Dict[str, Any]: """ Parse filter string into Frappe filter format. Supported operators: =, !=, <, >, <=, >=, like, not_like, in, not_in, is, is_not, between Examples: - "status:Unreconciled" -> {"status": "Unreconciled"} - "amount:>:100" -> {"amount": [">", 100]} - "name:like:%test%" -> {"name": ["like", "%test%"]} - "status:in:Open|Closed" -> {"status": ["in", ["Open", "Closed"]]} - "date:between:2025-01-01|2025-12-31" -> {"date": ["between", ["2025-01-01", "2025-12-31"]]} - "field:is:null" -> {"field": ["is", "not set"]} - "date:>=:2024-01-01,date:<=:2024-01-31" -> {"date": [[">=", "2024-01-01"], ["<=", "2024-01-31"]]} """ filters_dict: Dict[str, Any] = {} # Handle multiple filters separated by commas filter_parts = filter_str.split(',') for part in filter_parts: part = part.strip() if ':' in part: # Split on first two colons to handle operators with underscores components = part.split(':', 2) if len(components) >= 3: # Format: field:operator:value(s) field, operator, value_str = components[0].strip(), components[1].strip(), components[2] # Create the filter condition filter_condition = None # Handle special operators if operator.lower() in ['in', 'not_in']: # Handle list values separated by | values = [v.strip() for v in value_str.split('|')] # Convert numbers in list converted_values = [] for v in values: converted_values.append(_convert_value(v)) filter_condition = [operator.replace('_', ' '), converted_values] elif operator.lower() == 'between': # Handle range values separated by | range_values = [v.strip() for v in value_str.split('|')] if len(range_values) == 2: converted_range = [_convert_value(v) for v in range_values] filter_condition = [operator, converted_range] else: raise ValueError(f"Between operator requires exactly 2 values separated by |, got: {value_str}") elif operator.lower() in ['is', 'is_not']: # Handle null checks: is:null, is:not_null, is_not:null, etc. if value_str.lower() in ['null', 'none', 'empty']: filter_condition = [operator.replace('_', ' '), "not set"] elif value_str.lower() in ['not_null', 'not_none', 'not_empty']: filter_condition = [operator.replace('_', ' '), "set"] else: filter_condition = [operator.replace('_', ' '), _convert_value(value_str)] elif operator.lower() == 'not_like': # Handle not like operator filter_condition = ["not like", value_str] else: # Standard operators: =, !=, <, >, <=, >=, like filter_condition = [operator, _convert_value(value_str)] # Handle multiple filters for the same field if field in filters_dict: # Convert existing single filter to list format if not isinstance(filters_dict[field], list) or len(filters_dict[field]) != 2 or not isinstance(filters_dict[field][0], list): filters_dict[field] = [filters_dict[field]] # Add new filter condition filters_dict[field].append(filter_condition) else: filters_dict[field] = filter_condition elif len(components) == 2: # Simple field:value format (implies equality) field, value_str = components[0].strip(), components[1] filter_condition = _convert_value(value_str) # Handle multiple filters for the same field if field in filters_dict: # Convert existing single filter to list format if not isinstance(filters_dict[field], list) or len(filters_dict[field]) != 2 or not isinstance(filters_dict[field][0], list): filters_dict[field] = [filters_dict[field]] # Add new filter condition filters_dict[field].append(filter_condition) else: filters_dict[field] = filter_condition # Post-process: Convert >= and <= on same field to between operator filters_dict = _optimize_range_filters(filters_dict) return filters_dict