Shopping List MCP Server
by bbwrl
README.md
# Shopping List App
A simple shopping list app built with [Next.js 15](https://nextjs.org) (App Router). Every product belongs to a person, can be marked as purchased, and can be deleted.
This project is intentionally small — it's a learning/exercise project for IMS Praxis 5.
## Features
- Add products, mark them as purchased, delete them
- Filter by person
- Persistence via a simple JSON file (no database server required)
- Three ways to work with the data:
- **Server Actions** – used directly by the frontend (`src/app/actions.ts`)
- **REST API** – available under `/api/products`, e.g. for external clients or `curl`
- **MCP server** – exposes the same data as MCP tools (e.g. for ChatGPT) by calling the REST API
## Tech Stack
- [Next.js 15](https://nextjs.org) / React 19, App Router
- TypeScript
- No database, no ORM – persistence via a JSON file (`data/products.json`)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) via `mcp-handler`, using the Streamable HTTP transport
## Getting Started
```bash
npm install
npm run dev
```
Open the app at [http://localhost:3000](http://localhost:3000).
No configuration or `.env` file is needed to run the app locally. See [Environment Variables](#environment-variables) for the one optional setting used by the MCP server.
## Project Structure
```text
src/
app/
page.tsx # Home page (Server Component), loads products server-side
actions.ts # Server Actions: addProductAction, togglePurchasedAction, deleteProductAction
api/
products/
route.ts # GET /api/products, POST /api/products
[id]/route.ts # GET/PATCH/DELETE /api/products/:id
[transport]/
route.ts # MCP endpoint (Streamable HTTP), served at /api/mcp
components/
ProductForm.tsx # Add-product form (uses a Server Action)
ProductList.tsx # List incl. toggle/delete (uses Server Actions)
lib/
productRepository.ts # the only place that touches the filesystem (data/products.json)
mcp/
server.ts # registers the MCP tools
shoppingApiClient.ts # MCP's only way to reach the data — calls the REST API, never the repository directly
types/
product.ts # Product type
data/
products.json # data store (created automatically if missing)
```
## Data Model
```ts
interface Product {
id: string;
name: string;
person: string;
purchased: boolean;
createdAt: string; // ISO date
}
```
## Persistence
All products live in `data/products.json`. All file access is encapsulated in `src/lib/productRepository.ts` — neither the UI nor the API routes read or write the file directly. The repository exposes:
```ts
getProducts()
getProductsByPerson(person)
getProductById(id)
addProduct(product)
updateProduct(id, changes)
deleteProduct(id)
```
**Note:** This file-based persistence is intentionally just a **prototype/development solution**. On Vercel (and other serverless platforms) the local filesystem is not reliably persistent across requests or deployments — writes can be lost. For production use, `productRepository.ts` should be replaced with a real, persistent database (e.g. [Turso](https://turso.tech)). Since the rest of the app (UI, Server Actions, API routes) only ever talks to the data through the exported repository functions, that swap only touches this one file.
## Frontend ↔ Backend
The frontend (`page.tsx`, `ProductForm`, `ProductList`) uses **Next.js Server Actions** (`src/app/actions.ts`) to create, update, and delete products. There is no `fetch` call in the client — the Server Actions call the repository directly and then trigger a refresh of the server-rendered data via `revalidatePath("/")`.
The REST API under `/api/products` is independent and can be used separately (e.g. by external tools, scripts, or for testing) — it reads and writes the same data source.
## REST API
### Read products
```http
GET /api/products
GET /api/products?person=Rinaldo # filter by person, case-insensitive
GET /api/products/:id
```
### Add a product
```http
POST /api/products
Content-Type: application/json
{ "name": "Milk", "person": "Rinaldo" }
```
`id`, `purchased` (`false`), and `createdAt` are set automatically.
### Update a product
```http
PATCH /api/products/:id
Content-Type: application/json
{ "purchased": true }
```
Not all fields need to be provided (`name`, `person`, `purchased` are each optional and independently updatable).
### Delete a product
```http
DELETE /api/products/:id
```
### Error responses
```json
{ "error": "Product not found" }
```
| Case | Status |
|--------------------------------|--------|
| Invalid/empty request | 400 |
| Unknown ID | 404 |
| Internal error | 500 |
### `curl` examples
```bash
# Add a product
curl -X POST http://localhost:3000/api/products \
-H "Authorization: Bearer $SHOPPING_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Milk","person":"Rinaldo"}'
# List a person's products
curl "http://localhost:3000/api/products?person=Rinaldo" \
-H "Authorization: Bearer $SHOPPING_API_KEY"
# Mark a product as purchased
curl -X PATCH http://localhost:3000/api/products/PRODUCT_ID \
-H "Authorization: Bearer $SHOPPING_API_KEY" \
-H "Content-Type: application/json" \
-d '{"purchased":true}'
# Delete a product
curl -X DELETE http://localhost:3000/api/products/PRODUCT_ID \
-H "Authorization: Bearer $SHOPPING_API_KEY"
```
## MCP Server
A [Model Context Protocol](https://modelcontextprotocol.io) server exposes the shopping list to MCP clients (e.g. ChatGPT). It talks **only** to the REST API above — never to `productRepository.ts` or `data/products.json` directly — so it stays independent of whatever persistence backend the API uses.
```text
MCP client → MCP server → REST API → productRepository → data/products.json
```
**Endpoint:** `/api/mcp` (Streamable HTTP transport), implemented in `src/app/api/[transport]/route.ts` via [`mcp-handler`](https://www.npmjs.com/package/mcp-handler).
**Tools:**
| Tool | Description |
|---|---|
| `list_products` | List products, optionally filtered by person |
| `add_product` | Add a product for a person |
| `update_product` | Update name/person/purchased of a product |
| `mark_product_purchased` | Convenience tool to mark a product as (not) purchased |
| `delete_product` | Delete a product |
Requires the same bearer token as the REST API (see [Authentication](#authentication)). Test it locally with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```bash
npx @modelcontextprotocol/inspector --cli http://localhost:3000/api/mcp --method tools/list \
--header "Authorization: Bearer $SHOPPING_API_KEY"
```
## Environment Variables
| Variable | Required | Description |
|---|---|---|
| `SHOPPING_API_BASE_URL` | No | Base URL the MCP server uses to call the REST API. Defaults to `http://localhost:3000` locally, or `https://$VERCEL_URL` on Vercel. Set explicitly if you use a custom domain in production. |
| `SHOPPING_API_KEY` | **Yes** | Shared secret required as `Authorization: Bearer <key>` by the REST API and the MCP endpoint. Requests without a matching token are rejected. |
See `.env.example`.
## Authentication
The REST API and the MCP endpoint both require a bearer token — a single shared secret configured via `SHOPPING_API_KEY`. There is no per-user login; this is a simple static-token check suitable for a prototype, not full OAuth.
```bash
curl http://localhost:3000/api/products \
-H "Authorization: Bearer $SHOPPING_API_KEY"
```
A request with a missing or wrong token gets `401 Unauthorized`. If `SHOPPING_API_KEY` isn't set on the server at all, requests are rejected with `500` (fail closed, not open).
Server Actions (`src/app/actions.ts`) are unaffected — they call `productRepository` directly on the server and never go through the REST API, so they don't need a token.
## Known Limitations
- No authentication/authorization on either the REST API or the MCP server — anyone can see and edit all products. Planned as a follow-up.
- Concurrent writes are serialized within a single process (a simple queue in `productRepository.ts`), which is fine for a prototype but not for production multi-instance deployments.
- As noted above, persistence is not deployment-safe on serverless platforms like Vercel — a real database (e.g. [Turso](https://turso.tech)) is the intended next step.
## Deploy
The app can be deployed like any Next.js project, e.g. on [Vercel](https://vercel.com/new). Before using it in production, the data persistence layer (see above) should be swapped out for a real database.
More on Next.js: [Next.js Documentation](https://nextjs.org/docs) · [Learn Next.js](https://nextjs.org/learn)
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues