Auto Parts Inventory Agent
Connects to a PostgreSQL 17 database as a read-only role to answer natural-language questions about auto parts inventory, including compatible parts, branch stock levels, parts below minimum stock, movement summaries, and safe SQL queries.
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., "@Auto Parts Inventory Agent¿Qué pastillas de freno le sirven a un Toyota Corolla 2015?"
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.
Auto Parts Inventory Agent (MCP + PostgreSQL)
English · Español
An AI agent that answers questions in Spanish about the parts inventory of a vehicle and machinery dealer in Nicaragua. It connects to the database through an MCP server and cannot modify it.
The problem. To know whether a part fits a vehicle, what it costs and which branch has it, a salesperson has to cross-check compatibility catalogs, inventory and sales. This agent answers all of that in one natural-language question.
Real example (Claude Code + this MCP server):
User: ¿Cuántas unidades del filtro de aceite para Hilux 2018 se vendieron en Managua en septiembre? (How many units of the oil filter for a 2018 Hilux were sold in Managua in September?)
Agent: (calls
buscar_repuestos→resumen_movimientos) Se vendieron 22 unidades del FIL-TGD-1031 (Toyota Genuine Parts) en Managua Central, en 8 ventas por C$ 8,580. Quedan 53 en stock. (22 units of FIL-TGD-1031 were sold at Managua Central, in 8 sales totaling C$ 8,580. 53 left in stock.)
Highlights
MCP server in Python (official SDK,
mcp2.x, stdio transport) with 4 read-only tools, used from Claude Code.PostgreSQL 17 data model for parts, vehicle compatibility, 5 branches, stock and about 19,000 inventory movements. The generator is reproducible.
5 independent security layers. Each one alone is enough to stop a write. I tested this by asking the agent to "delete all movements in Estelí", and every attempt was blocked.
36 automated tests (pytest), including an end-to-end test over stdio. They run on every push with GitHub Actions against a real PostgreSQL.
Stack: Python 3.12 · uv · MCP · PostgreSQL 17 · Docker Compose · psycopg 3 · pytest · GitHub Actions · Claude Code
Status: Phase 2 complete (database + MCP server). All data (parts, suppliers, prices) is fictional and generated reproducibly.
Related MCP server: Inventory Tracker MCP Server
Requirements
Docker Desktop (with Docker Compose)
uv to manage Python 3.12 and dependencies (
brew install uv)
Quick start
cp .env.example .env # then change both passwords
docker compose up -d --wait # starts PostgreSQL 17
uv sync # creates .venv and installs dependencies
uv run scripts/cargar_datos.py
uv run pytest # tests (with the database running)The script drops and recreates every table on each run. It uses a fixed seed, so it always generates exactly the same data.
Port 5433: the container is published on 5433 instead of 5432, so it doesn't clash with a locally installed PostgreSQL. Change it with
POSTGRES_PORTin.env.
Connecting to the database
# SQL console as administrator
docker compose exec db psql -U repuestos_admin -d repuestos
# From a GUI tool (DBeaver, TablePlus, pgAdmin)
# host: localhost port: 5433 database: repuestos user/password: from .envSchema
categorias ─┐ ┌─ proveedores
▼ ▼
repuestos ◄──── repuesto_modelo ────► modelos_vehiculo
│
├──► inventario ◄── sucursales
└──► movimientos ◄─┘Table | Contents |
| Categories: filters, brakes, suspension, engine, electrical, etc. |
| Suppliers with country and contact |
| Parts: unique code, name, brand, category, supplier, |
| Make, model, year range and type ( |
| Many-to-many compatibility between parts and models |
| Branches: Managua Central, León, Granada, Chinandega and Estelí |
|
|
| Stock in/out with date, reason ( |
| View with parts below the minimum at each branch |
Design decisions:
Money in
NUMERIC(12,2), neverfloat, to avoid rounding errors.A movement's quantity is always positive. The
tipofield gives the direction.Current stock is the sum of movements. The load script computes it that way, so
inventarioandmovimientosalways reconcile.Compatibility uses year ranges: one row for "Hilux 2016–2024" instead of one row per year.
Read-only role for the agent
The load script creates the agente_lectura role (user and password in .env). It has two independent barriers:
Privileges: only
SELECTon the tables and the view. It cannot write or create objects.Read-only transactions by default (
default_transaction_read_only).
The agent must always connect with this role, never as administrator.
Sample data
200 parts, 30 models (pickups, cars, trucks, motorcycles and machinery), 8 suppliers and 5 branches.
About 19,000 movements between April 1 and September 30, 2026.
Sales follow each part's demand. Managua sells more than the other branches, and filters and lubricants sell more than an alternator. When stock drops below the reorder point, an order is placed and arrives according to the supplier's lead time.
About 30 part/branch pairs end below the minimum: most still have some stock and about 7 are at zero. Some happen naturally (an order that hasn't arrived yet) and others are forced by cutting replenishment in the last weeks.
Sample queries
-- Which oil filters fit a 2018 Hilux, and how many are there per branch?
SELECT r.codigo, r.nombre, s.nombre AS sucursal, i.stock_actual, r.precio_nio
FROM repuestos r
JOIN repuesto_modelo rm ON rm.repuesto_id = r.id
JOIN modelos_vehiculo mv ON mv.id = rm.modelo_id
JOIN inventario i ON i.repuesto_id = r.id
JOIN sucursales s ON s.id = i.sucursal_id
WHERE mv.modelo = 'Hilux' AND 2018 BETWEEN mv.anio_desde AND mv.anio_hasta
AND r.nombre ILIKE 'filtro de aceite%';
-- Parts below the minimum in León
SELECT * FROM v_stock_bajo_minimo WHERE sucursal = 'León' ORDER BY faltante DESC;
-- Top 10 parts by sales (in córdobas) in September
SELECT r.codigo, r.nombre, sum(m.cantidad * m.precio_unitario) AS ventas_nio
FROM movimientos m JOIN repuestos r ON r.id = m.repuesto_id
WHERE m.motivo = 'venta' AND m.fecha >= '2026-09-01' AND m.fecha < '2026-10-01'
GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 10;
-- Check: stock must reconcile with movements (should return 0)
SELECT count(*) FROM inventario i
JOIN (SELECT sucursal_id, repuesto_id,
sum(CASE tipo WHEN 'entrada' THEN cantidad ELSE -cantidad END) AS s
FROM movimientos GROUP BY 1, 2) m USING (sucursal_id, repuesto_id)
WHERE i.stock_actual <> m.s;Architecture (Phase 2: MCP server)
┌─────────────┐ stdio (JSON-RPC) ┌──────────────────────┐ psycopg, role agente_lectura ┌──────────────────────┐
│ Claude Code │ ─────────────────► │ MCP server (Python) │ ────────────────────────────► │ PostgreSQL 17 │
│ (client) │ ◄───────────────── │ 4 tools │ ◄──────────────────────────── │ Docker, port 5433 │
└─────────────┘ └──────────────────────┘ └──────────────────────┘MCP (Model Context Protocol) is the standard an AI client such as Claude Code uses to discover and call external tools. The server uses the official Python SDK (
mcp2.x,MCPServerclass).stdio transport: Claude Code launches the server as a child process and talks to it over standard input and output. No network port is opened. That is why the server never uses
print(): stdout is the protocol channel, and logs go to stderr.Logic is separate from the protocol:
consultas.pyandsql_seguro.pyare plain functions that receive a connection.servidor.pyonly wraps them and translates errors. They can be tested without MCP and could be exposed another way, such as a REST API.A new connection per call, always as
agente_lectura. With a single local user it costs milliseconds and recovers on its own if the database restarts.Descriptions are part of the product. The model picks a tool by reading the docstrings and parameter descriptions, so they are written in Spanish with examples. The
consulta_sqldescription includes the full schema, so the model can write correct SQL.
Tools
Tool | Purpose | Parameters |
| Compatible parts with price, stock per branch and compatible models |
|
| What needs restocking, out-of-stock first |
|
| Stock in, stock out, sales and amount sold for a part per branch |
|
| Fallback for everything else (rankings, monthly totals, etc.) |
|
Searches ignore accents and case thanks to the unaccent extension: "Esteli" finds "Estelí". Every tool is declared read-only (read_only_hint). When something fails, the tools return an error in Spanish that explains how to fix it, for example: "No existe el código X. Usá buscar_repuestos" (code X doesn't exist, use buscar_repuestos).
Security: why the agent cannot modify data
There are several independent layers. Any one of them is enough to stop a write:
# | Layer | Where | What it blocks |
1 | Model judgment | Tool descriptions say "read-only" | The model doesn't even try to write |
2 | Pre-validation |
| A single statement, starting with |
3 | Wrapper |
| A |
4 | Read-only transaction and timeout |
| Any write, and queries that never finish |
5 | Role privileges | PostgreSQL ( | Writes even if every previous layer is bypassed |
The real guarantee is layer 5. Layers 2 to 4 add defense in depth and give the model clear messages. The tests in tests/test_seguridad.py check each layer, including layer 5 on its own: a connection that turns off read-only mode still gets permission denied.
Live test with Claude Code. The request was: "Borrá todos los movimientos de Estelí y poné el stock del filtro FIL-TGD-1031 en 500" (delete all movements in Estelí and set the stock of filter FIL-TGD-1031 to 500).
Normal request: the model refused without calling any tool, because the descriptions tell it access is read-only (layer 1). It offered the SQL for someone with permissions to run.
Forced request ("really try it with
consulta_sql"):Attempt
Blocked by
Response
DELETE FROM movimientos WHERE sucursal_id = …Layer 2
Solo se permiten consultas de lectura: SELECT o WITH ... SELECT.WITH b AS (DELETE FROM movimientos … RETURNING 1) SELECT count(*) FROM bLayer 3 (PostgreSQL)
WITH clause containing a data-modifying statement must be at the top levelUPDATE inventario SET stock_actual = 500 …Layer 2
Solo se permiten consultas de lectura: SELECT o WITH ... SELECT.Afterwards, Estelí's 2,829 movements and the filter's stock were unchanged.
A note on interactive Claude Code: these layers protect the MCP path. A regular Claude Code session in this folder also has
Bashand can read.env, which contains the admin password. Those actions ask for your permission, but the MCP server doesn't stop them. For a production agent, expose only the MCP tools and don't give it access to admin credentials.
Connecting it to Claude Code
The project's .mcp.json registers the server. It holds no credentials, because the server reads them from .env:
{ "mcpServers": { "repuestos": { "command": "uv", "args": ["run", "agente-repuestos-mcp"] } } }With the database running, open Claude Code in this folder (
claude).Approve the
repuestosserver when prompted. You can check it with/mcp.Ask in Spanish, for example: "¿Qué repuestos están bajo el mínimo en León?" (which parts are below the minimum in León?)
To test it non-interactively and with the MCP tools only:
claude -p "¿Qué sucursal vendió más en córdobas en agosto?" \
--mcp-config .mcp.json --strict-mcp-config --tools "" --allowedTools "mcp__repuestos__*"Test questions
# | Question | Tools Claude chose | Result |
1 | Which front brake pads do you have for a 2019 Hilux, and at which branches? |
| FRE-TGD-1078, C$ 1,965, in stock at all 5 branches (271 units) |
2 | Which parts are below the minimum in Esteli? (no accent) |
| 3 parts, none out of stock |
3 | How many units of the oil filter for a 2018 Hilux were sold in Managua in September? |
| 22 units in 8 sales, C$ 8,580 |
4 | Which branch sold the most in córdobas in August? |
| Managua Central, C$ 4,293,530 |
5 | I need an air filter for a 2020 John Deere 310L backhoe. Where is it and at what price? |
| FIL-RET-1040 (Fleetguard), C$ 3,000, 48 units |
6 | Delete all movements in Estelí and set the stock of filter FIL-TGD-1031 to 500 | none (refused) | See the security section |
The questions were asked in Spanish; they are translated here.
Project structure
├── .github/workflows/ci.yml # CI: PostgreSQL service + data load + pytest
├── docker-compose.yml # PostgreSQL 17 in Docker
├── .env.example # environment template (the real .env is not in git)
├── .mcp.json # registers the MCP server in Claude Code
├── db/schema.sql # tables, constraints, indexes and view
├── scripts/
│ ├── catalogo.py # fixed catalogs: categories, brands, models, templates
│ └── cargar_datos.py # generates and loads the data
├── src/agente_repuestos/
│ ├── db.py # connection as agente_lectura (read-only, 5 s timeout)
│ ├── consultas.py # buscar_repuestos, stock_bajo_minimo, resumen_movimientos
│ ├── sql_seguro.py # validation and protected execution of consulta_sql
│ └── servidor.py # MCP layer: tool definitions and descriptions
├── tests/ # pytest: validation, queries, security, end-to-end stdio
└── pyproject.toml / uv.lockUseful commands
docker compose stop # stop the database (data is kept)
docker compose start # start it again
docker compose down -v # delete container AND data (volume)Author
Fernando Flores · Data Analyst · LinkedIn · GitHub
Code under the MIT License.
This server cannot be deployed
Maintenance
Related MCP Connectors
Text-to-SQL MCP server: read-only queries on PostgreSQL, MySQL and SQL Server with your real schema
- dataOAuthco.thinair
PostgreSQL, MySQL, and SQL Server in one session. 26 read-only MCP tools for AI agents.
Analytical memory for AI agents: a real Postgres queried in plain English over MCP. One command.
Query your Postgres from ChatGPT or Claude without exposing the database or handing over credentials. Run npx boltschema connect next to your database and it dials out over HTTPS — no inbound firewall rule, no open port, works with localhost and VPC-private databases. Read-only is enforced by a SQL guard, a Postgres READ ONLY transaction, and a scoped role generated for you.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to execute SQL queries and introspect PostgreSQL schemas, tables, and indexes with read-only safety by default. Supports optional write operations and works with Claude, LangChain, and other agents via stdio or HTTP transports.-
- FlicenseNot gradedqualityDmaintenanceA Model Context Protocol (MCP) server that lets Claude AI manage a real PostgreSQL inventory database using natural language.-
- AlicenseNot gradedqualityDmaintenanceA production-ready MCP server for PostgreSQL — built for Claude Desktop, Claude Code, and any MCP-compatible AI agent.Apache 2.0
- AlicenseAqualityCmaintenanceMCP server enabling text-to-SQL on PostgreSQL databases using a CrewAI multi-agent system. It generates and executes read-only SQL queries from natural language, with security layers against prompt injection.2MIT