Car Buying Advisor MCP Server
Provides tools for querying and analyzing owner-review data for Audi, including brand statistics, rating trends by model year, common complaints and praises by topic, and full-text search over Audi reviews. Enables comparison of Audi against other luxury car brands.
Provides tools for querying and analyzing owner-review data for BMW, including brand statistics, rating trends by model year, common complaints and praises by topic, and full-text search over BMW reviews. Enables comparison of BMW against other luxury car brands.
Provides tools for querying and analyzing owner-review data for INFINITI, including brand statistics, rating trends by model year, common complaints and praises by topic, and full-text search over INFINITI reviews. Enables comparison of INFINITI against other luxury car brands.
Provides tools for querying and analyzing owner-review data for Mercedes-Benz, including brand statistics, rating trends by model year, common complaints and praises by topic, and full-text search over Mercedes-Benz reviews. Enables comparison of Mercedes-Benz against other luxury car brands.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Car Buying Advisor MCP Servercompare Audi and BMW on mileage and common complaints"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Car Buying Advisor — MCP Server + Ollama Chat Loop
A decision-support tool for choosing between luxury car brands from real
owner reviews: an MCP server exposing 6 deterministic, unit-testable tools
over precomputed analysis tables, plus a chat loop that drives a local
Ollama LLM (qwen2.5:7b-instruct) as a real MCP client of that server.
No tool calls an LLM internally — each is a plain Python function reading a pre-built CSV. The LLM synthesizes answers from tool results; comparison-by-aspect ("compare Audi and BMW on mileage") is achieved by the LLM composing multiple tool calls, not by a dedicated tool — see "Design notes" below.
Status: the MCP server and the Ollama chat loop are both working
end-to-end (see scripts/manual_test.py). The one piece not yet built is
a UI on top of the chat loop — everything below runs from the command line
until that exists.
Repository layout
The repo is organized to mirror the order the project was actually built in — raw data in, tables out, tools on top of the tables, chat loop on top of the tools:
eda/ 1. Exploratory analysis on the raw reviews
sentiment_classification/ 2. Picks and applies a sentiment model
topic_modelling/ 3. BERTopic topic modeling + aspect table
data/
raw/ Original review CSVs (input to step 1)
interim/ Intermediate pipeline artifacts
processed/ Final tables the MCP server reads
README.md What lives in each of the 3 layers above, and who produces/consumes it
mcp_server/ 4. The MCP server, its tools, and the Ollama client
scripts/ Throwaway manual-run scripts (not shipped code)
logs/ Structured JSON logs (git-ignored, created at runtime)Each stage's own README (where one exists) has the detail; this file is the map between them plus how to run everything.
Related MCP server: MarketCheck MCP Apps
1. EDA — eda/
Three notebooks establish dataset shape and quality, and produce the first
tables the MCP server reads directly (brand_stats.csv, rating_trends.csv,
cleaned_reviews.csv). Run in the order listed in
eda/README.md — that file also has the full
data-quality findings and how they informed later steps.
2. Sentiment classification — sentiment_classification/
sentimentclassification.ipynb evaluates candidate sentiment models
(compared against the gold-labeled 3,700-review set validated in step 1)
and applies the winning model to the full 38K reviews. This notebook runs
on Kaggle, not locally: /kaggle/working/... output paths are that
platform's own scratch directory and are left as-is, but the
/kaggle/input/.../car-review-chatbot/... input paths now mirror this
repo's data/raw and data/interim — i.e. the Kaggle Dataset itself
needs a raw/ and interim/ subfolder matching this repo before the
notebook will run (its local-relative outputs, e.g.
phase4_model_comparison.csv, are written under data/interim/). Its
sentiment predictions feed directly into topic modelling below. See
sentiment_classification/README.md
for the validation-set-vs-actual-dataset distinction, the 4 candidate
models compared, and exactly where the comparison results and final
predictions are stored.
3. Topic modelling — topic_modelling/
topicmodelling.ipynb (also run on Kaggle, same raw//interim/
Kaggle-Dataset-layout requirement as above) runs BERTopic over the review
text and joins the result with the sentiment predictions from step 2,
producing aspect_sentiment_table.csv — one row per review with its
topic, keywords, and sentiment. Downloaded from Kaggle's working
directory into data/processed/. See
topic_modelling/README.md for the full
pipeline, including the human topic-labeling step (39 labeled topics + 1
outlier bucket).
build_topic_info.py is a small local, deterministic script that derives
data/processed/phase5_topic_info.csv (one row per topic: id, label,
keywords, review count) from aspect_sentiment_table.csv. It's the only
step in the pipeline that runs locally rather than on Kaggle:
python3 -m venv .venv_mcp
source .venv_mcp/bin/activate
pip install -r requirements.txt
python topic_modelling/build_topic_info.pyAfter this step, all 5 tables the MCP server needs exist in
data/processed/:
File | Produced by |
|
|
|
|
|
|
|
|
|
|
4. MCP server + chat loop — mcp_server/
mcp_server/server.py and mcp_server/client.py run as two separate OS
processes, talking over the actual Model Context Protocol (stdio
transport, JSON-RPC) — not a plain in-process function call:
Ollama (qwen2.5:7b-instruct)
│ tool schemas ← fetched live from the MCP server's list_tools()
│ tool_calls (name + args)
▼
mcp_server/client.py (MCP client, process A)
│ MCP call_tool() over stdio (JSON-RPC)
▼
mcp_server/server.py (MCP server, process B — spawned as a subprocess)
│ validates arguments itself (from the same schema it published)
▼
pandas DataFrames (5 CSVs from data/processed/, loaded once at server startup)The tool JSON schemas handed to Ollama are not hand-maintained — they
come straight from the live server's list_tools() response, generated
from each tool function's own type-hinted signature (see
mcp_server/tools/, one file per tool). The server is the single source
of truth for what it can do; nothing in client.py duplicates that. See
mcp_server/README.md for the file-by-file
breakdown of that folder.
Logging across the process boundary: each process logs to its own file
(logs/app.log for the client/orchestrator, logs/mcp_server.log for the
tool server) — two independent RotatingFileHandlers rotating the same
file from separate processes can race and corrupt it. A request_id set in
client.py does not automatically appear in the server's log lines —
contextvars don't cross an OS process boundary. Today, matching a chat
turn's server-side tool logs to its client-side request_id means lining up
timestamps between the two files (scripts/manual_test.py prints a
reminder of this). The correct fix is to thread request_id through MCP's
per-call meta field and have server-side tools accept an injected
Context parameter to log through the protocol itself — a real upgrade,
deliberately deferred rather than done as a rushed addition to
already-tested tool code.
Setup and running
python3 -m venv .venv_mcp
source .venv_mcp/bin/activate
pip install -r requirements.txt
# One-time (step 3 above, repeated here for convenience):
python topic_modelling/build_topic_info.py
# Requires Ollama running locally with the model pulled:
# ollama pull qwen2.5:7b-instruct
python scripts/manual_test.pyscripts/manual_test.py spawns mcp_server/server.py as a subprocess
itself — you do not run server.py separately. All data is loaded into
memory once, at the server subprocess's startup. If a required file is
missing or a required column isn't present, that subprocess exits
immediately with a fatal error instead of starting silently broken
(visible in logs/mcp_server.log and in the client's stderr).
Edit the QUERY constant at the top of scripts/manual_test.py to try
different questions (single-tool, compound/multi-tool, unknown-brand). It
prints the model's answer, every tool call made (name, args, status,
duration), and the client-side structured log lines for that exact run.
Tool → data file map
Tool | Reads | Purpose |
|
| Avg/median rating, review count, rating std, avg model year for one brand |
|
| Same stats for N brands, side by side (calls |
|
| Top negative-sentiment topics for a brand, optionally narrowed to one aspect |
|
| Same, for positive-sentiment topics |
|
| Avg rating per model year, optionally filtered by brand and/or year range |
|
| Case-insensitive substring search over raw review text |
data/processed/phase5_topic_info.csv is a reference table (topic_id,
topic_label, top_keywords, review_count) derived from
aspect_sentiment_table.csv by topic_modelling/build_topic_info.py. It
isn't a primary source for any tool — it's used to validate/fuzzy-match a
caller's topic_filter string against the 40 real BERTopic labels, and to
tell the caller which topics do exist when their filter doesn't match
anything.
Data coverage
brand_stats.csv, rating_trends.csv, cleaned_reviews.csv, and
aspect_sentiment_table.csv all cover the same 5 brands: Audi, BMW,
INFINITI, Lexus, Mercedes-Benz (31,938 reviews total). Brand name matching
is case-insensitive; an unrecognized brand always returns an explicit
brand_not_found status plus the list of brands that do exist — never an
empty list or a crash.
topic_filter matching
get_common_complaints / get_common_praises resolve topic_filter in two
passes:
Substring match against
topic_labelandtop_keywords(case-insensitive) — e.g."mileage"matches the topic labeled "high mileage & maintenance reliability".Fuzzy match (
difflib.get_close_matches, cutoff 0.4) against knowntopic_labelvalues, for near-miss spellings, if the substring pass finds nothing.
If neither pass matches, the tool returns status="topic_not_found" plus
available_topics (all 40 known labels) instead of silently returning [].
Error handling contract
Every tool returns a Pydantic model with a status field — never a bare
None, an empty list with no explanation, or an unhandled exception:
Unknown brand →
status="brand_not_found", plusavailable_brands.Unknown/unmatched topic →
status="topic_not_found", plusavailable_topics.Valid brand/topic but zero matching rows →
status="no_results", plus a human-readablemessageexplaining why.Otherwise →
status="ok"with the populated result.
Design notes: why there's no "compare by aspect" tool
A query like "Compare Audi and BMW on mileage" is not a single tool
call. There is no compare_brands_by_topic tool because it would just
re-implement calling get_common_praises / get_common_complaints twice
each (once per brand) and returning the same data those tools already
return. Instead, the LLM composes:
get_common_praises("Audi", topic_filter="mileage")
get_common_complaints("Audi", topic_filter="mileage")
get_common_praises("BMW", topic_filter="mileage")
get_common_complaints("BMW", topic_filter="mileage")and synthesizes the comparison itself from the four results. Adding a dedicated 7th tool would duplicate logic that composition of the existing 6 tools already covers — the tools are kept single-purpose and composable by design.
Testing without an LLM
Every tool is still a plain importable function (no subprocess needed), so it's unit-testable directly by importing the tool modules in-process:
import sys
sys.path.insert(0, "mcp_server")
from tools import get_brand_stats, compare_brands, get_common_complaints, get_common_praises, get_rating_trends, search_reviews
get_brand_stats("bmw") # case-insensitive
get_brand_stats("Toyota") # -> brand_not_found
compare_brands(["Audi", "BMW"])
get_common_complaints("Audi", topic_filter="mileage")
get_common_praises("Audi", topic_filter="interior")
get_rating_trends("Audi", year_range=(2015, 2018))
search_reviews("transmission", brand_name="Audi", limit=3)Testing the full MCP + LLM loop
source .venv_mcp/bin/activate
python scripts/manual_test.pyWhat's left
The MCP server and the Ollama-driven chat loop are both fully working
end-to-end today (scripts/manual_test.py exercises the real thing). The
only remaining piece is a UI on top of mcp_server/client.py's chat loop
— that's the next thing to build, not yet started.
This server cannot be deployed
Maintenance
Related MCP Connectors
US vehicle recalls, complaints, EPA figures, VIN decode and trouble codes, with sources.
ReviewOracle - 8 review intel tools: sentiment, themes, competitors, response drafts.
Automotive design reference search with filters, natural language, image retrieval, and comparison.
Mechanic-grade used-car listing verdicts: risk score, failure points, repair costs, fair price.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceProvides comprehensive tools for enterprise searching, channel statistical analysis, and dealer network mapping to help users evaluate market coverage and competitive positioning. It enables detailed industry benchmarking and provides insights into sales strength, brand influence, and distribution efficiency.2-
- FlicenseNot gradedqualityBmaintenanceProvides 25 interactive automotive intelligence tools for real-time market data, including VIN decoding, price predictions, and inventory analytics. It enables AI assistants to perform car searches, trade-in estimations, and market trend analysis using the Model Context Protocol.2-
- FlicenseNot gradedqualityDmaintenanceProvides structured dealer brand data including inventory, promotions, reviews, and dealer profile via MCP tools, enabling LLMs to answer accurate brand-related queries.-
- FlicenseNot gradedqualityCmaintenanceEnables discovery and analysis of automotive brand regional sales data, including brand details, regional store layouts, model sales rankings, and dealer information.1-