mcp-pricescout
In this project, you are going to make a chatbot to scrape LLM Inference Serving websites to research costs of serving various LLMs. You will do this by writing an MCP Server that hooks up to Firecrawl's API and saving the data in a SQLite Database. You should use the following websites to scrape:
- "cloudrift": "https://www.cloudrift.ai/inference"
- "deepinfra": "https://deepinfra.com/pricing"
- "fireworks": "https://fireworks.ai/pricing#serverless-pricing"
- "groq": "https://groq.com/pricing"
1. Make a venv with uv
2. Sync venv with pyproject.toml (`uv sync`)
3. Make an API Key on Anthropic and Firecrawl
4. Complete the 2 tool calls in `starter_server.py`
5. Change the `server_config.json` to point to your server file
6. Complete any section in `starter_client.py` that has "#complete".
7. Test using any methods taught in the course
8. Use the following prompts in your chatbot but play around with all the LLM providers in the list above:
- "How much does cloudrift ai (https://www.cloudrift.ai/inference) charge for deepseek v3?"
- "How much does deepinfra (https://deepinfra.com/pricing) charge for deepseek v3"
- "Compare cloudrift ai and deepinfra's costs for deepseek v3"
## Environment notes
Two dependency-drift issues surfaced against the packages actually resolved at
implementation time (Aug 2026), fixed rather than worked around:
- **`firecrawl-py`'s `.scrape()` has no `success` key.** The currently-installed
`firecrawl-py` (v4.38.0, the only version whose `.scrape(url, formats=...)`
method matches this assignment's shape) raises on failure and returns a
`Document` with no `success` field on success — unlike the older API this
assignment's instructions describe. `starter_server.py`'s
`scrape_result.get("success", True)` defaults to `True` instead of `False`
so real successful scrapes are actually detected (an explicit
`success: false`, if a future/older version ever returns one, still works).
- **`mcp-server-sqlite` crashes against the latest `mcp` package.** Its last
release (Apr 2025) pins `mcp[cli]>=1.6.0` with no upper bound, and current
`mcp` releases removed the `Server.list_resources` decorator it depends on.
`server_config.json`'s sqlite entry pins a compatible `mcp` version for that
one subprocess only, via `uvx --with "mcp<1.10" mcp-server-sqlite ...` — this
doesn't affect the `mcp` version this project itself uses.
If using a Vocareum-proxied Anthropic key (`voc-...`), set
`ANTHROPIC_BASE_URL=https://claude.vocareum.com` in `.env` alongside
`ANTHROPIC_API_KEY` — `starter_client.py` reads it and passes it to the
Anthropic client; leaving it unset uses the standard Anthropic API.
## Verified live
Real end-to-end run, evidence in [evidence/](evidence/) — full transcript, real scraped
content, and what each piece of it proves. All four required items are demonstrated,
including a real Firecrawl scrape (`Successfully scraped N out of N websites`). Three real
bugs surfaced only by actually running this, none visible in code review or offline testing:
- **`load_dotenv()` doesn't override an already-set environment variable by default.** This
machine has an ambient user-level `ANTHROPIC_BASE_URL` pointing at the standard API
(unrelated to this project), which silently took precedence over `.env`'s
Vocareum URL — every request went to `api.anthropic.com` with a `voc-...` key and got a
clean `401 invalid x-api-key`. Fixed with `load_dotenv(override=True)`, which is the
correct behavior for a project's own `.env` regardless of ambient state.
- **`process_query`'s tool-use loop broke when a single response contained more than one
`tool_use` block** — a real, common pattern (Claude often batches several tool calls in
one turn). The original loop called `self.anthropic.messages.create()` again *inside* the
`for content in response.content:` loop, reassigning `response` while that same `for` loop
kept iterating the *old* content list — so a second tool_use in the same response never got
its matching tool_result appended, and the next API call failed with `400: tool_use ids
were found without tool_result blocks immediately after`. Fixed by collecting every block
of a response (text to print, all tool_use calls to execute) before making the next call,
and sending all of that turn's tool_results together in one message, matching what the
API actually requires.
- **`mcp-server-sqlite`'s `read_query` returns Python `repr()`-style output, not JSON** —
single-quoted (`"[{'company_name': 'x', ...}]"`), which looks like JSON at a glance but
makes `json.loads()` fail immediately (`Expecting property name enclosed in double
quotes`). `show_stored_data` now parses it with `ast.literal_eval()` instead, which
handles the format actually being sent.
TDQS
Scored across 2 tools
The two tools have clearly distinct roles: one initiates the scraping workflow and the other retrieves previously scraped data. There is minor potential for confusion about what 'extract' means in the naming, but the descriptions resolve the boundary.
Both tools follow a consistent verb_object pattern with snake_case: scrape_websites and extract_scraped_info. The naming style is uniform and predictable.
Two tools is on the low end for a price-scouting server, but the pair covers a minimal scrape-then-retrieve workflow. It feels thin rather than bloated, so it is borderline acceptable.
The server appears aimed at price monitoring, but there is no tool for searching, comparing, updating, or deleting scraped data. The surface only supports scraping and retrieving stored content, leaving significant gaps for a typical price-scout use case.