Skip to main content
Glama
GRaghavend

Car Buying Advisor MCP Server

by GRaghavend

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.py

After this step, all 5 tables the MCP server needs exist in data/processed/:

File

Produced by

brand_stats.csv

eda/EDA.ipynb

rating_trends.csv

eda/EDA.ipynb

cleaned_reviews.csv

eda/EDA_NPL_38K.ipynb

aspect_sentiment_table.csv

topic_modelling/topicmodelling.ipynb (Kaggle)

phase5_topic_info.csv

topic_modelling/build_topic_info.py

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.py

scripts/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

get_brand_stats(brand_name)

data/processed/brand_stats.csv

Avg/median rating, review count, rating std, avg model year for one brand

compare_brands(brand_names)

data/processed/brand_stats.csv

Same stats for N brands, side by side (calls get_brand_stats per brand)

get_common_complaints(brand_name, topic_filter=None, top_n=5)

data/processed/aspect_sentiment_table.csv, data/processed/phase5_topic_info.csv

Top negative-sentiment topics for a brand, optionally narrowed to one aspect

get_common_praises(brand_name, topic_filter=None, top_n=5)

data/processed/aspect_sentiment_table.csv, data/processed/phase5_topic_info.csv

Same, for positive-sentiment topics

get_rating_trends(brand_name=None, year_range=None)

data/processed/rating_trends.csv

Avg rating per model year, optionally filtered by brand and/or year range

search_reviews(query, brand_name=None, min_rating=None, limit=20)

data/processed/cleaned_reviews.csv

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:

  1. Substring match against topic_label and top_keywords (case-insensitive) — e.g. "mileage" matches the topic labeled "high mileage & maintenance reliability".

  2. Fuzzy match (difflib.get_close_matches, cutoff 0.4) against known topic_label values, 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", plus available_brands.

  • Unknown/unmatched topic → status="topic_not_found", plus available_topics.

  • Valid brand/topic but zero matching rows → status="no_results", plus a human-readable message explaining 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.py

What'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.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides 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
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides 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
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables discovery and analysis of automotive brand regional sales data, including brand details, regional store layouts, model sales rankings, and dealer information.
    1
    -