Orders MCP server
README.md
# MCP vs API: one orders service, two interfaces
Companion repo for the video **"MCP vs API: why do we need MCP if REST already works?"**
Clone it, run two commands, and do the same job twice. Once with a plain REST API. Once
with an MCP server on top of it. Takes about 20 minutes.
---
## What we are building
You run a small online store. Orders come in. Some get stuck and never ship. You want an AI
agent to find the stuck ones and open a GitHub issue for each.
That is the whole example. One small, real job.
**The first way**, you give the agent your API docs and let it use curl. It has to figure
out which endpoint to call, build a date filter for "more than 7 days", notice the response
comes in pages, and convert cents to dollars.
**The second way**, you give it a tool called `find_stale_orders` that takes
`{ older_than_days: 7 }`.
Both call the same endpoint, `GET /orders`. The store does not change at all. What changes
is who does the thinking: the agent, or your server.
```
┌──────────────────────────────────┐
Web frontend ───────▶│ │
Mobile app ───────▶│ Orders service (Express) │
Microservice ───────▶│ GET /orders │
│ GET /orders/:id │
│ PATCH /orders/:id │
└──────────────▲───────────────────┘
│ plain HTTP, nothing AI specific
┌──────────────┴───────────────────┐
Claude Code ───────▶│ Orders MCP server │
Cursor ───────▶│ tool: find_stale_orders │
Codex ───────▶│ input: { older_than_days: 7 } │
└──────────────────────────────────┘
```
Your API is the door. MCP gives AI clients a standard handle to open it with.
The orders service never knows that Claude Code exists. The MCP server is just another HTTP
client of your API. The only difference is that it describes itself in a way agents
understand.
---
## Try it in one minute
You need Node 20 or newer. Nothing else. No database, no API keys.
```bash
git clone https://github.com/bytemonk-academy/mcp-vs-api.git
cd mcp-vs-api
npm install
npm test
```
`npm test` runs 31 tests against both the REST API and the MCP server. If they pass,
everything works and the rest is just you watching it happen.
Now start the service and leave it running:
```bash
npm run api
```
In a second terminal, look at the data:
```bash
npm run orders
```
```
ID CUSTOMER STATUS PLACED DAYS TOTAL
----------------------------------------------------------------------
ORD-1001 Ada Lovelace UNSHIPPED 2026-07-27 31 $129.00
ORD-1002 Grace Hopper UNSHIPPED 2026-08-03 24 $45.99
...
Showing 20 of 24 matching orders.
!! There are more. page.nextOffset = 20
You have NOT seen all 24 orders.
```
Then ask it the question this whole demo is about:
```bash
npm run orders -- --stale=7
```
Eight orders. Same eight on any machine, at any time of day.
---
## What is `npm run orders`?
It is a shortcut for curl.
It sends `GET /orders` to your API and prints the reply as a table instead of raw JSON.
That is all it does. You can run the same request yourself:
```bash
curl "http://localhost:3000/orders"
```
You get the same data, just harder to read. The script is only there so you can check the
data quickly. It is not part of the lesson. In Phase 1 the agent gets curl and the docs,
nothing else.
It takes a few options:
```bash
npm run orders -- --stale=7 # unshipped for more than 7 days
npm run orders -- --status=UNSHIPPED # filter by status
npm run orders -- --limit=5 --offset=5 # move through the pages by hand
```
---
## Why the test data looks the way it does
There are 24 orders, kept in memory, with dates set relative to today. So there are always
exactly 8 stale orders, whenever you clone this.
Three problems are put there on purpose, so you can see the difference yourself instead of
taking the video's word for it:
- **The reply comes in pages.** Ask for orders and you get 20 out of 24. Nothing in those
20 rows looks incomplete. An agent that stops at the first page gives a wrong answer and
sounds sure about it.
- **Some old orders are cancelled.** They look stale but they are not. If you filter on
`shippedAt` instead of `status`, you count them by mistake.
- **Some orders sit just under the 7 day line.** Count the days slightly wrong and you get
a wrong total, not an error message.
The MCP server deals with all three in code, once, in `src/mcp/server.ts`. In the curl
version, the agent has to get all three right every single time.
---
## The exercise
Do these in order. Phase 1 before Phase 2 is the point, because the difference is the
lesson.
| | Guide | What you do |
|---|---|---|
| **Phase 1** | [docs/phase-1-rest-only.md](docs/phase-1-rest-only.md) | Give the agent your API docs, let it use curl, watch what it has to work out by itself |
| **Phase 2** | [docs/phase-2-mcp.md](docs/phase-2-mcp.md) | Turn on the Orders MCP server and GitHub's, run the same prompt again |
| **After** | [docs/architecture.md](docs/architecture.md) | What changed, what did not, and when MCP is not worth it |
Also here: [the API reference](docs/orders-api.md) you give the agent in Phase 1,
[prompts you can copy](docs/prompts.md), and [troubleshooting](docs/troubleshooting.md).
Phase 2 opens real GitHub issues, so use a test repo you do not mind filling up.
---
## What is in here
```
src/
data/orders.ts The 24 test orders
api/app.ts The REST API. Knows nothing about MCP.
api/server.ts Starts it on a port.
mcp/server.ts The MCP server. Calls the REST API over HTTP.
scripts/orders.ts The table viewer used above
clients/ Plain MCP clients, in Python and TypeScript
tests/ Tests for both halves
docs/ The walkthrough
.mcp.json Claude Code reads this automatically
.cursor/mcp.json Cursor reads this automatically
```
Three tools. Each one is a thin wrapper over an endpoint you already have:
| Tool | Input | Calls |
|---|---|---|
| `find_stale_orders` | `{ older_than_days: 7 }` | `GET /orders?status=UNSHIPPED&before=...`, through every page |
| `get_order` | `{ order_id: "ORD-1001" }` | `GET /orders/ORD-1001` |
| `mark_order_shipped` | `{ order_id: "ORD-1001" }` | `PATCH /orders/ORD-1001` |
[`src/mcp/server.ts`](src/mcp/server.ts) is about 170 lines and most of it is comments.
That is all an MCP server really is.
---
## Seeing the protocol for yourself
Claude Code does nothing special here. It starts the server as a subprocess and sends
JSON-RPC messages over stdin and stdout.
[`clients/raw_mcp_client.py`](clients/raw_mcp_client.py) does the same thing by hand:
```python
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("find_stale_orders", {"older_than_days": 7})
```
The same script then talks to GitHub's MCP server over HTTP to open the issues:
```python
await session.call_tool("create_issue", {"owner": owner, "repo": name, "title": ...})
```
Same shape both times. One server is a Node process on your laptop. The other is run by
GitHub. The client cannot tell them apart. That is the part worth remembering. There is a
TypeScript version in [`clients/`](clients/README.md) if you would rather stay in one
language.
---
## Tests
```bash
npm test
```
31 tests. The MCP ones drive a real MCP client over stdio, the same way Claude Code does.
Worth reading if you plan to write your own server. They show what is actually worth
checking: that every tool has a usable description and schema, that paging really works,
that a 404 comes back as a tool error instead of a crash, and that cancelled orders stay
out of the results.
---
## Commands
```bash
npm run api # REST API on :3000
npm run api:dev # same, restarts when you edit a file
npm run orders # print the orders as a table
npm run mcp # run the MCP server directly (agents usually do this for you)
npm test # the tests
npm run typecheck # tsc --noEmit
npm run inspect # MCP Inspector, to try the tools by hand
```
`npm run inspect` is the fastest way to see exactly what an agent sees: tool names,
descriptions, and the input schema for each one.
The data is kept in memory, so restarting `npm run api` puts everything back to the start.
---
## When is MCP worth it?
Phase 1 works. That is not a trick. A good agent will find the stale orders and open the
issues using nothing but curl and your docs. MCP is not what makes the job possible.
What it changes is the shape of the integration. How to query your orders service now lives
in one server, instead of in every agent's context window. The same capability works in
Claude Code, Cursor, and Codex without writing a new integration for each one. And you
choose which capabilities to expose, which is very different from handing over an API key.
What it does not change: authentication, authorization, validation, rate limiting, retries,
and good service design are all still your job. An MCP server on top of a badly designed
API is still a badly designed API.
Roughly, the value grows with the number of clients times the number of tools. One agent
calling two functions you control? Skip it, just call the functions. Thirty tools across
five teams and four clients? That is when a shared protocol starts to pay for itself.
[docs/architecture.md](docs/architecture.md) covers where the line sits.
---
MIT licensed. Use it in your own teaching, no credit needed.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues