qbo_search_customers
Search QuickBooks Online customers by display name using a substring, case-insensitive query. Returns matching customers and total count, with optional limit on results.
Instructions
Search customers by display name (substring, case-insensitive).
Args:
query: Free-text fragment matched against Customer.DisplayName
via QBO's LIKE '%query%' operator.
limit: Cap on returned customers (1-1000, default 50).
Returns: JSON envelope: {"ok": true, "data": {"customers": [...], "count": N}}.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | ||
| limit | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- qbo_mcp/server.py:76-102 (handler)The MCP tool handler for 'qbo_search_customers' – registered via @mcp.tool() decorator. Validates input, caps limit to 1-1000, delegates to QBOClient.search_customers(), and returns a JSON envelope.
@mcp.tool() def qbo_search_customers(query: str, limit: int = 50) -> str: """Search customers by display name (substring, case-insensitive). Args: query: Free-text fragment matched against Customer.DisplayName via QBO's `LIKE '%query%'` operator. limit: Cap on returned customers (1-1000, default 50). Returns: JSON envelope: {"ok": true, "data": {"customers": [...], "count": N}}. """ if not query or not query.strip(): return _err("query is required and must be non-empty") try: capped = max(1, min(limit, 1000)) customers = _get_client().search_customers(query=query.strip(), limit=capped) return _ok( { "customers": customers, "count": len(customers), "query": query.strip(), "limit": capped, } ) except (ValueError, QBOError, RuntimeError) as exc: return _err(str(exc)) - qbo_mcp/server.py:76-77 (registration)The @mcp.tool() decorator registers 'qbo_search_customers' as a FastMCP tool on the 'qbo-mcp' server.
@mcp.tool() def qbo_search_customers(query: str, limit: int = 50) -> str: - qbo_mcp/client.py:308-324 (helper)QBOClient.search_customers() – builds a QBO query with DisplayName LIKE '%<escaped>%', delegates to _paginate_query, and returns a list of customer dicts.
def search_customers( self, query: str, *, limit: int = 50 ) -> list[dict]: """Search active customers by display name (substring, case-insensitive).""" if not query or not query.strip(): raise ValueError("query must be non-empty") safe = _escape_qbo_string(query.strip()) where = f"DisplayName LIKE '%{safe}%'" return list( self._paginate_query( select_clause="SELECT * FROM Customer", entity="Customer", where=where, order_by="DisplayName", limit=limit, ) ) - qbo_mcp/client.py:260-304 (helper)QBOClient._paginate_query() – handles STARTPOSITION/MAXRESULTS pagination for QBO queries, yielding records up to the requested limit.
def _paginate_query( self, *, select_clause: str, entity: str, where: str | None = None, order_by: str | None = None, limit: int, page_size: int = QUERY_PAGE_SIZE, ) -> Iterator[dict]: """Yield records from a QBO query, handling STARTPOSITION pagination. QBO's query endpoint requires explicit `STARTPOSITION` and `MAXRESULTS` clauses for paging (it never returns a cursor). Stops yielding once `limit` records have been emitted, or when a page returns fewer rows than `page_size` (signaling the natural end of results). """ if limit <= 0: return emitted = 0 start_position = 1 while True: page = max(1, min(page_size, limit - emitted)) stmt_parts = [select_clause] if where: stmt_parts.append(f"WHERE {where}") if order_by: stmt_parts.append(f"ORDER BY {order_by}") stmt_parts.append(f"STARTPOSITION {start_position}") stmt_parts.append(f"MAXRESULTS {page}") stmt = " ".join(stmt_parts) data = self._query_page(stmt) qr = data.get("QueryResponse") or {} items = qr.get(entity) or [] for record in items: yield record emitted += 1 if emitted >= limit: return if len(items) < page: return start_position += len(items) - qbo_mcp/client.py:465-478 (helper)_escape_qbo_string() – escapes backslashes, single quotes, and underscores for safe QBO string literals in LIKE expressions.
def _escape_qbo_string(value: str) -> str: """Escape characters that would break a QBO string literal. QBO's query language uses single quotes for strings and a backslash as the escape character for both `'` and `\\`. `%` and `_` are also metacharacters inside `LIKE` expressions; we leave `%` alone so that callers don't lose substring semantics, but escape `_` so a literal underscore in a customer name doesn't silently match anything. """ return ( value.replace("\\", "\\\\") .replace("'", "\\'") .replace("_", "\\_") )