Skip to main content
Glama
AraksyaG

SmartPark Central MCP Server

by AraksyaG
README.md
# SmartPark Central — MCP Server for Confirmed Reservations (Stage 3)

An intelligent assistant for a parking facility, built with **LangChain** and
**LangGraph** on a **Retrieval-Augmented Generation (RAG)** architecture. The bot
answers questions about the garage, looks up live prices / opening hours /
availability, interactively collects reservation details, and protects personal
data with a guardrails layer.

This repository is **Stage 3** of the EPAM final project. It builds on the Stage 1
RAG chatbot and the Stage 2 human-in-the-loop approval flow by adding an **MCP (Model
Context Protocol) server** that records every **approved** reservation to a text file
in the format `Name | Car Number | Reservation Period | Approval Time`. The server is
secured (shared-token auth, input sanitisation, path confinement, atomic locked
writes). When the administrator approves a request, the confirmed booking is written
automatically — via the MCP server or an equivalent direct function call.

---

## Features

| Requirement (brief) | Where it lives |
|---|---|
| RAG chatbot architecture | `graph.py` (LangGraph agent), `vector_store.py`, `tools.py` |
| Vector database for information | **Milvus** (`vector_store.py`; Milvus Lite locally) |
| Static/dynamic data split *(optional bonus)* | Static → Milvus (`knowledge_base.py`); dynamic (prices, hours, availability) → **SQLite/SQLAlchemy** (`dynamic_db.py`) |
| Provide information to users | retriever tool + 3 dynamic-data tools (`tools.py`) |
| Collect user inputs for reservations | `reservation.py` (validated slot-filling) + `submit_reservation` tool |
| Guardrails against sensitive-data exposure | `guardrails.py` (Microsoft Presidio + regex fallback) |
| Evaluation (Recall@K, Precision, latency) | `evaluation/evaluate.py` |
| **Second agent for the administrator** | `admin_agent.py` (compose request + interpret reply) |
| **Escalate reservation to a human** | `tools.py::submit_reservation` → `reservation_store` + `channels` |
| **Send request / receive decision** | `channels.py` (memory/email/REST) + `admin_service.py` (FastAPI) |
| **Maintain agent-to-agent communication** | reservation lifecycle in `reservation_store.py`, status tool |
| **MCP server to write data to file** | `mcp_server.py` (FastMCP) + `recorder.py` (secure core) |
| **Record on approval** (`Name \| Car \| Period \| Time`) | `admin_service.py` approval hook → `recorder`/`mcp_client` |
| **Secure & resistant to unauthorised access** | token auth, input sanitisation, path confinement, file locking |

---

## MCP server (Stage 3)

**MCP (Model Context Protocol)** is an open standard for exposing tools to AI apps.
Our server (`mcp_server.py`, built with the official SDK's `FastMCP`) exposes one tool,
`record_reservation`, that appends a confirmed booking to a text file. The write logic
(`recorder.py`) is shared by the MCP path and a direct function-call fallback.

```bash
poetry run parking-mcp-server         # run the MCP server (stdio transport)
```

Flow: administrator approves a request (Stage 2 API) → the approval hook records it →
a line is appended to `data/confirmed_reservations.txt`:

```
Anna Smith | AB12CD | 2026-07-15 09:00 to 18:00 | 2026-07-11T10:00:00+00:00
```

**Security measures**
- **Token auth** — the recorder rejects writes without the correct `MCP_AUTH_TOKEN`.
- **Input sanitisation** — `|`, `\n`, `\r` are stripped so a value can't forge extra
  columns/rows (format injection).
- **Path confinement** — the target file must resolve inside the project `data/` dir,
  blocking path traversal (`../../etc/...`).
- **Atomic, locked appends** — a thread lock plus an OS advisory file lock prevent
  corrupted/interleaved lines under concurrent approvals.

Set `USE_MCP=true` to record through the MCP server (client↔server over stdio);
`USE_MCP=false` (default) uses the equivalent direct call. The full stdio round-trip is
covered by an e2e test (`RUN_MCP_E2E=1 pytest`).

---

## Human-in-the-loop flow (Stage 2)

```
 user ──▶ chatbot (agent 1) ──collect details──▶ submit_reservation
                                                     │
                                       save PENDING record (reservation_store)
                                                     │
                                       notify admin (channel: memory/email/REST)
                                                     ▼
                          administrator ──▶ Admin REST API (/pending, /decision)
                                                     │
                                 AdminAgent (agent 2) interprets the reply
                                                     ▼
                                    reservation → APPROVED / REFUSED
                                                     │
 user ──ask status──▶ chatbot ──check_reservation_status──▶ reports outcome
```

### Running the admin API

```bash
poetry run parking-admin-api          # serves http://localhost:8001
```

Example (the admin approves a request):

```bash
TOKEN=change-me-admin-token           # set ADMIN_API_TOKEN in .env for real use
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8001/pending
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"reply":"approve, looks fine","admin":"robert"}' \
     http://localhost:8001/reservations/SP-ABC123/decision
```

The `reply` field can be free text ("yes, that's fine" / "no, we're full") — the
**AdminAgent** interprets it into an `approved` / `refused` decision.

---

## Architecture

```
                 ┌──────────────────────────── LangGraph agent ───────────────────────────┐
  user  ──▶  query_or_respond ──(tool needed?)──▶  tools  ──▶  generate ──▶  answer  ──▶ user
                    │  (LLM decides)                  │            │ (grounded + PII-redacted)
                    │                                 │
                    └── no tool: answer directly      ├─ search_parking_info ─▶ Milvus (static KB)
                       (e.g. next reservation Q)      ├─ get_parking_prices   ─┐
                                                      ├─ get_working_hours     ├▶ SQLite (dynamic)
                                                      ├─ check_availability    ─┘
                                                      └─ submit_reservation    ─▶ validated request
```

- **Static knowledge** (general info, location, rules, booking process, FAQ) is
  embedded with `text-embedding-3-small` and stored in **Milvus**.
- **Dynamic data** (prices, hours, live availability) lives in **SQLite** and is
  queried exactly — never embedded, never hallucinated.
- The **LLM** (Azure OpenAI `gpt-4o-mini`) chooses which tool to call, then writes a
  grounded answer. Every retrieved chunk and every final answer passes through the
  **guardrails** so personal data cannot leak.

---

## Project structure

```
Stage1_RAG_Chatbot/
├── src/parking_chatbot/
│   ├── config.py          # settings from .env (pydantic-settings)
│   ├── llm.py             # Azure chat + embedding model factories
│   ├── knowledge_base.py  # load + chunk static markdown docs
│   ├── vector_store.py    # Milvus build/load/retriever
│   ├── dynamic_db.py      # SQLite schema, seeding, queries
│   ├── guardrails.py      # PII detection + redaction (Presidio / regex)
│   ├── reservation.py     # validated reservation model + slot-filling
│   ├── tools.py           # LangChain tools bound to the agent
│   ├── graph.py           # LangGraph agent (query_or_respond→tools→generate)
│   ├── chatbot.py         # high-level Chatbot facade
│   └── cli.py             # terminal chat REPL
├── data/
│   ├── static/            # knowledge base (markdown) → vector DB
│   └── dynamic/           # SQLite DB is created here
├── evaluation/            # eval dataset + Recall@K / Precision / MRR / latency
├── scripts/ingest.py      # build vector store + seed SQL DB
├── tests/                 # pytest suite (offline, LLM mocked)
├── docker-compose.yml     # optional full Milvus server
└── .github/workflows/ci.yml
```

---

## Setup

> **Prerequisites:** Python 3.11/3.12, and — for anything that calls the model —
> an Azure OpenAI resource **reachable from the EPAM VPN**.

```bash
# 1. install dependencies (Poetry)
poetry install

# 2. configure credentials
cp .env.example .env
#   then edit .env and fill in AZURE_OPENAI_API_KEY / AZURE_OPENAI_ENDPOINT

# 3. build the knowledge base + dynamic DB  (needs VPN for embeddings)
poetry run parking-ingest
```

`MILVUS_URI` defaults to a local file (`./milvus_lite.db`) — **Milvus Lite**, so no
server is needed. To use a full Milvus server instead:

```bash
docker compose up -d          # starts Milvus on localhost:19530
# set MILVUS_URI=http://localhost:19530 in .env, then re-run parking-ingest
```

---

## Usage

```bash
poetry run parking-chat
```

Example conversation:

```
You: what are your opening hours?
Assistant: SmartPark Central is open 24 hours a day, 7 days a week.

You: how much is daily parking for a normal car?
Assistant: Standard car parking is 24.00 USD per day.

You: I want to book a space
Assistant: Sure! What is your first name?
... (collects first name, last name, car number, period, confirms, submits)

You: what's the manager's phone number?
Assistant: I'm sorry, I can't share staff contact details. Please call our support
           hotline on +1-555-0100.
```

---

## Evaluation

```bash
poetry run python evaluation/evaluate.py --k 3
```

Produces `evaluation/results/report.md` and `metrics.json` with **Recall@K**,
**Precision@K**, **MRR** and **latency** (mean / p95). See the report for the
per-question breakdown.

---

## Testing

```bash
poetry run pytest            # 27 tests, fully offline (LLM + Milvus mocked)
poetry run pytest --cov=parking_chatbot
```

The suite never contacts Azure or a Milvus server, so it runs in CI without a VPN.

---

## Guardrails / data protection

Personal data (staff phones, existing customer records, licence plates) may exist in
the knowledge base. The guardrails layer redacts it **twice**: once when retrieved
context is serialised for the model, and again on the final answer. Two backends:

- **Presidio** (default): pretrained spaCy NLP model detects `PERSON` etc., plus
  pattern recognisers for email / phone / credit card and a custom licence-plate
  recogniser.
- **Regex fallback** (`USE_PRESIDIO=false`): no model download; covers email, phone,
  credit card and plates. Used in CI.

---

## Tech choices in one line each

- **LangChain + LangGraph** — required by the brief; LangGraph gives an explicit,
  inspectable state machine with built-in conversation memory.
- **Milvus** — a production vector DB explicitly recommended by the brief; Milvus
  Lite makes local dev/testing zero-setup.
- **SQLite/SQLAlchemy for dynamic data** — exact, cheap-to-update structured answers
  where embeddings would be the wrong tool.
- **Azure OpenAI `gpt-4o-mini` + `text-embedding-3-small`** — the models available
  through EPAM, matching the course material.
- **Presidio** — the open-source standard for PII detection using pretrained NLP.