Side project · MCP server · 2026
Pulling Google Reviews in one tool call.
I built a Model Context Protocol server that samples Google Reviews across a company's locations in one tool call. It fetches the best, worst, newest, and most relevant reviews, retries failed requests, and caches the result in Postgres.

Why this problem
The official SerpAPI MCP server exposes low-level tools. An agent has to search for a place, fetch a page of reviews, paginate, change the sort order, and repeat the process for each location. That is a lot of bookkeeping for a question like “summarise this company's Google Reviews”.
I tested the server against Madrona Bakery in 2026. The agent made two MCP calls, and the server made eleven SerpAPI requests behind the scenes. The result came back in about twelve seconds. Those numbers belong to that test run, not every request.
The sequence does not change much from one company to the next, so I moved it into review_mcp. The agent asks for an overview. The server handles the API calls and returns one answer.
One concern per layer
The MCP layer contains no HTTP, SQL, or provider-specific code. It only turns service methods into tools. The business logic lives in google_reviews/. A shared MapsProvider protocol covers the different SerpAPI and SearchAPI response shapes, while a repository keeps Postgres out of the service layer.
I started with SearchAPI because it was cheaper, but its results varied too much between identical requests. Switching to SerpAPI only required changing SEARCH_PROVIDER because both clients implement the same protocol.
MCP Client (Claude · Claude Code · Cursor)
│ Streamable-HTTP
▼
mcp_server/ : FastMCP wrapper, 6 tool definitions, zero business logic
│
▼
google_reviews/ : orchestration · retry · caching · domain models
├─▶ SerpAPI / SearchAPI (swappable via SEARCH_PROVIDER)
└─▶ PostgreSQL (versioned snapshots, keyed by company domain)The MCP tools pass requests to CompanyReviewService. That service coordinates the selected provider and Postgres repository.
- mcp_server
- google_reviews.services
- google_reviews.providers
- google_reviews.models
- google_reviews.storage
- FastMCP
- Pydantic 2
- tenacity
- psycopg
Six tools, one of them does the work
Each tool has a markdown version for agents and a _raw version for code that needs structured JSON. Most requests can use get_company_overview. The narrower tools are useful when fetching one place or one sort order is enough.
| Tool | Returns | Purpose |
|---|---|---|
search_company_places | markdown | Find matching Google Maps locations for a company URL. |
search_company_places_raw | json | Same place discovery, structured for programmatic clients. |
get_place_reviews | markdown | Fetch N reviews for one location, one sort order; page sizes hint at SerpAPI billing breakpoints. |
get_place_reviews_raw | json | Same review fetch, structured. |
get_company_overview | markdown | Sample up to three locations across four sort orders in one call. |
get_company_overview_raw | json | Same overview, structured. |
Under the hood
Boring, on purpose
The tool definitions are short. Most of the code deals with failures around them. Tenacity retries 429s, 5xx responses, and timeouts with exponential backoff and jitter. Pydantic validates responses with frozen=True and extra="forbid". If a provider changes its response shape, the server raises a parse error instead of quietly dropping data.
Before calling a provider, the service checks whether Postgres already has enough reviews for the request. It stores versioned snapshots under the company's domain. For place discovery, it tries the website domain first, followed by the company name and then the domain prefix.
The server closes its connection pool through atexit.register(). It uses stateless HTTP, so requests do not depend on process-local session state.
What shipped
Tech stack
- Python 3.12 with mypy, Ruff, and unit tests enforced in CI
- FastMCP server over Streamable-HTTP, stateless mode
- SerpAPI + SearchAPI behind a shared
MapsProviderprotocol - PostgreSQL 16 for versioned review snapshots, via psycopg
- Pydantic 2, tenacity, requests; uv for dependency management
- Docker Compose for one-command local stand-up
Design decisions
- Push the orchestration into the server, not onto the agent
- One concern per layer: MCP, service, providers, storage stay separate
- Cache by domain so the same question doesn't cost twice
- Fail loudly on provider-shape changes via strict Pydantic models
- Keep deferred work visible in the README and case study
What I'd do next
- Move the I/O path to httpx + psycopg-async once load justifies it
- Add a proper migration story (currently
CREATE TABLE IF NOT EXISTS) - Outer per-tool timeout, instead of relying on the retry budget
- A second MCP server in the same shape, against a different data source, to see whether the pattern travels
The architecture diagram is adapted from the project's companion blog post, Building the bridges your AI needs, in the same repo under blog/post.md.