Skip to main content
Glama

Consumer Complaint Database MCP

complaints.py9.71 kB
""" complaints.py – local-only MCP server for querying U.S. CFPB consumer-complaint data. This file is a refactor of the original `server.py`. The **only** transport it supports is stdio, so tools like Claude Desktop can launch it directly without exposing an HTTP port. Dependencies (declare these in pyproject.toml or install with uv/pip): mcp[cli] >= 1.2.0 httpx >= 0.27.0 # async HTTP client (could be requests if you prefer) python-dateutil >= 2.9.0 # for robust date parsing Typical one-time setup (from the project root -– see earlier explanation): uv add "mcp[cli]" httpx python-dateutil uv run complaints.py # smoke test – waits for stdin/stdout client """ from __future__ import annotations import datetime as _dt from dataclasses import dataclass from typing import Annotated, List, Optional import httpx from dateutil import parser as date_parser from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.exceptions import ToolError # --------------------------------------------------------------------------- # # 1. MCP server instance # --------------------------------------------------------------------------- # mcp = FastMCP( "Banking Complaints", description=( "Use this server whenever you need data from the CFPB Consumer Complaint " "Database. Tip: pass the company name *exactly* as it appears in the " "CFPB 'company' field (e.g., 'PNC Bank N.A.')." ), ) # --------------------------------------------------------------------------- # # 2. Internal helpers # --------------------------------------------------------------------------- # _CFPB_ENDPOINT = ( "https://www.consumerfinance.gov/data-research/consumer-complaints/search/api/v1/" ) # Valid product categories accepted by the CFPB API _PRODUCT_CATEGORIES: list[str] = [ "Checking or savings account", "Credit card", "Credit reporting or other personal consumer reports", "Debt collection", "Money transfer, virtual currency, or money service", "Mortgage", "Payday loan, title loan, or personal loan", "Vehicle loan or lease", "Student loan", "Other financial service", ] # Top 20 formal bank names accepted by the CFPB _TOP_20_BANKS: list[str] = [ "JPMorgan Chase Bank, N.A.", "Bank of America, N.A.", "Wells Fargo Bank, N.A.", "Citibank, N.A.", "U.S. Bank National Association", "PNC Bank N.A.", "Truist Bank", "Goldman Sachs Bank USA", "TD Bank, N.A.", "Capital One Bank (USA), N.A.", "Charles Schwab Bank, SSB", "Fifth Third Bank, National Association", "The Bank of New York Mellon", "State Street Bank and Trust Company", "BMO Bank N.A.", "Ally Bank", "Citizens Bank, N.A.", "Regions Bank", "KeyBank National Association", "Huntington National Bank", "Santander Holdings USA, Inc.", "Navy Federal Credit Union", "Rocket Mortgage, LLC", ] @dataclass(slots=True) class Complaint: complaint_id: str date_received: str date_sent_to_company: str | None product: str sub_product: str | None issue: str sub_issue: str | None consumer_complaint_narrative: str consumer_consent_provided: str | None company: str company_public_response: str | None company_response_to_consumer: str | None timely_response: str | None consumer_disputed: str | None state: str | None zip_code: str | None submitted_via: str | None tags: list[str] | None @classmethod def from_json(cls, item: dict) -> "Complaint": attrs = { "complaint_id": item.get("complaint_id"), "date_received": item.get("date_received"), "date_sent_to_company": item.get("date_sent_to_company"), "product": item.get("product"), "sub_product": item.get("sub_product"), "issue": item.get("issue"), "sub_issue": item.get("sub_issue"), "consumer_complaint_narrative": item.get("consumer_complaint_narrative") or "", "consumer_consent_provided": item.get("consumer_consent_provided"), "company": item.get("company"), "company_public_response": item.get("company_public_response"), "company_response_to_consumer": item.get("company_response_to_consumer"), "timely_response": item.get("timely_response"), "consumer_disputed": item.get("consumer_disputed"), "state": item.get("state"), "zip_code": item.get("zip_code"), "submitted_via": item.get("submitted_via"), "tags": item.get("tags"), } return cls(**attrs) async def _fetch_cfpb(params: dict) -> List[Complaint]: async with httpx.AsyncClient(timeout=10) as client: resp = await client.get(_CFPB_ENDPOINT, params=params) if resp.status_code != 200: raise ToolError(f"CFPB API error {resp.status_code}: {resp.text[:200]}") data = resp.json() results = data.get("hits", {}).get("hits", []) return [Complaint.from_json(hit["_source"]) for hit in results] def _date_to_iso(date_like: str | _dt.date | None) -> Optional[str]: if date_like is None: return None if isinstance(date_like, _dt.date): return date_like.isoformat() return date_parser.parse(str(date_like)).date().isoformat() # --------------------------------------------------------------------------- # # 3. Public MCP tool # --------------------------------------------------------------------------- # @mcp.tool( name="search_complaints", description=( "Search the U.S. Consumer Financial Protection Bureau public complaint " "database and return recent complaints as structured objects." " Pass `narrative_only=true` if you need complaints that include consumer narrative text." ), ) async def search_complaints( *, text: Annotated[ str | None, "Full-text query (matched across narrative, product, and issue fields). " "Leave blank to fetch most recent complaints.", ] = None, company: Annotated[ str | None, "Exact company string as shown in the CFPB database. Common examples: " "'JPMorgan Chase Bank, N.A.', 'Bank of America, N.A.', 'Wells Fargo Bank, N.A.', " "'Citibank, N.A.', 'U.S. Bank National Association', 'PNC Bank, N.A.', " "'Truist Bank', 'Goldman Sachs Bank USA', 'TD Bank, N.A.', " "'Capital One Bank (USA), N.A.', 'Charles Schwab Bank, SSB', " "'Fifth Third Bank, National Association', 'The Bank of New York Mellon', " "'State Street Bank and Trust Company', 'BMO Bank N.A.', 'Ally Bank', " "'Citizens Bank, N.A.', 'Regions Bank', 'KeyBank National Association', " "'Huntington National Bank'." ] = None, product: Annotated[ str | None, "Exact CFPB product category (e.g., 'Mortgage', 'Credit card', 'Auto loan or lease', " "'Checking or savings account')." ] = None, since: Annotated[ str | None, "Only return complaints received **on or after** this date " "(ISO 8601, e.g., '2024-01-01'). Mutually exclusive with `days`.", ] = None, days: Annotated[ int | None, "Shorthand to set `since` to N days ago (e.g., 30). " "Ignored if `since` is provided.", ] = 30, size: Annotated[ int, "Maximum number of results to return (1 – 100).", ] = 20, narrative_only: Annotated[ bool, "Set to `true` to return only complaints that include consumer narrative text." ] = False, ) -> object: """ Query parameters mirror the public CFPB API. Returns ------- List[Complaint] Up to `size` complaint objects sorted by date_received descending. """ if not (1 <= size <= 100): raise ToolError("`size` must be between 1 and 100") if since and days: raise ToolError("Provide either `since` **or** `days`, not both") # Resolve date filter if since: since_iso = _date_to_iso(since) else: since_iso = _date_to_iso(_dt.date.today() - _dt.timedelta(days=days or 0)) # Build search params es_query: dict[str, object] = { "size": size, "sort": "created_date_desc", } # Map parameters to CFPB API query fields if text: es_query["search_term"] = text es_query["field"] = "all" if company: es_query["company"] = company if product: es_query["product"] = product if since_iso: es_query["date_received_min"] = since_iso if narrative_only: es_query["has_narrative"] = "true" results = await _fetch_cfpb(es_query) if not results: message = ( "The query produced zero results. This often happens when the bank name " "is too casual or the product category is misidentified.\n\n" "* Valid product categories:\n - " + "\n - ".join(_PRODUCT_CATEGORIES) + "\n\n" "* Top 20 formal bank names accepted by the CFPB:\n - " + "\n - ".join(_TOP_20_BANKS) ) return {"system_message": message} return results # --------------------------------------------------------------------------- # # 4. Entrypoint (stdio transport only) # --------------------------------------------------------------------------- # if __name__ == "__main__": # Claude Desktop (and any other stdio-aware client) will exec: # uv run complaints.py # which drops us into this launcher. mcp.run(transport="stdio")

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/IngeniousIdiocy/Consumer-Complaint-Database-MCP'

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