mcp-rest-bridge
# mcp-rest-bridge
An MCP server that lets Claude (or any MCP client) work with a store through its REST API: "what's running
low?", "where is order 1007?", "how did we do in the last 30 days?", "we just got 20 drills, add them".
It comes with a small sample store API (products, customers, orders in SQLite) so the whole thing runs locally
in a minute. Every name and number in that data is made up.
This is a personal portfolio project. What it shows is the pattern: a typed, tested MCP layer in front of an
existing HTTP API, with safe writes and errors the model can act on.
## Features

Read tools:
| Tool | What it answers |
| --- | --- |
| `search_products` | Find products by name/SKU text and/or category, with paging |
| `get_product` | One product's price, stock and reorder level |
| `list_low_stock` | Products at or below their reorder level (or a threshold you give) |
| `get_order` | Order status, customer, line items, total |
| `list_customer_orders` | A customer's orders, newest first |
| `sales_summary` | Revenue, order count, average order, status breakdown, top products for a date range |
Write tools, only registered when `STORE_ALLOW_WRITES=true`:
| Tool | What it does |
| --- | --- |
| `adjust_stock` | Add or remove units with a reason; logged in a `stock_movements` table |
| `create_order` | Pending order at list prices for an existing customer; reserves the stock |
Both write tools are dry runs unless the call says `confirm: true`. The dry run goes through the real API
inside a transaction that is rolled back, so the preview applies the same rules as the real write (no negative
stock, enough stock for every line, customer exists). The tool tells the model to show the preview and ask the
user before confirming.

Also:
- Resource `store://categories` and resource template `store://products/{sku}`
- Prompt `restock_report`: has the model build a purchase list from `list_low_stock`
- Every tool has a zod input schema and an output schema and returns `structuredContent` plus JSON text
- Two transports: stdio (Claude Desktop, Claude Code) and Streamable HTTP (stateless, bearer token required,
DNS-rebinding protection from the SDK when bound to localhost)
- Upstream failures become tool errors with a next step instead of stack traces:
| Upstream | What the model sees |
| --- | --- |
| 404 | `Product ZZZ-999 not found. Use search_products to find a valid SKU` |
| 409 | `Not possible: Only 0 of TLS-005 in stock, 1 requested.` |
| 401 | `The store API rejected the credentials (401). The server operator should check STORE_API_KEY.` |
| timeout | `Store API did not answer within 5000 ms. Try again, or ask for a smaller result.` |
| down | `Could not reach the store API at http://... The API may be down.` |
| 5xx / bad JSON | `The store API failed (502). Try again later.` (details go to stderr) |

## Architecture
```
Claude Desktop / Claude Code / any MCP client
│ stdio, or Streamable HTTP + bearer token (src/stdio.ts, src/http.ts)
▼
src/mcp/ server.ts (read tools, resources, prompt), write-tools.ts (dry run / confirm),
│ tool-result.ts (ApiError -> tool error text), http-app.ts (HTTP endpoint)
▼
src/client/ StoreApiClient: fetch + timeout + bearer token, zod-validated responses,
│ ApiError with a `kind` (not_found, conflict, timeout, ...)
▼ HTTP
src/api/ sample store REST API: Express 5 + node:sqlite, seeded sample data
```
`src/domain.ts` holds the zod schemas shared by the API, the client and the tools' output schemas, so a change
in the API's response shape shows up as a type error, and at runtime as a clear `bad_response` error.
## Run it
Needs Node 22.13 or newer (uses the built-in `node:sqlite`).
```bash
npm install
npm run build
# terminal 1: the sample API (data resets on every start unless DATABASE_PATH is a file)
API_KEY=change-me-to-something-long npm run api
# terminal 2: try the MCP server with the MCP Inspector CLI
npx @modelcontextprotocol/inspector --cli node dist/stdio.js \
-e STORE_API_URL=http://127.0.0.1:4000/api -e STORE_API_KEY=change-me-to-something-long \
--method tools/call --tool-name list_low_stock
```
All settings are in [.env.example](.env.example).
### Claude Desktop
Add this to `claude_desktop_config.json` (absolute path to your checkout), start the API, restart Claude:
```json
{
"mcpServers": {
"store": {
"command": "node",
"args": ["/absolute/path/to/mcp-rest-bridge/dist/stdio.js"],
"env": {
"STORE_API_URL": "http://127.0.0.1:4000/api",
"STORE_API_KEY": "change-me-to-something-long",
"STORE_ALLOW_WRITES": "false"
}
}
}
}
```
Claude Code:
```bash
claude mcp add store -e STORE_API_URL=http://127.0.0.1:4000/api -e STORE_API_KEY=change-me-to-something-long \
-- node /absolute/path/to/mcp-rest-bridge/dist/stdio.js
```
### Over HTTP
```bash
STORE_API_KEY=change-me-to-something-long MCP_HTTP_TOKEN=another-long-random-token npm run mcp:http
# POST http://127.0.0.1:3000/mcp with "Authorization: Bearer another-long-random-token"
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:3000/mcp --transport http \
--header "Authorization: Bearer another-long-random-token" --method tools/list
```
Binding to anything other than localhost turns off the SDK's DNS-rebinding check; put it behind HTTPS and a
reverse proxy in that case.
## Pointing it at your own API
The MCP layer only knows `StoreApiClient`, so connecting a real system means changing two files:
1. `src/domain.ts`: describe your API's responses with zod (or reuse your OpenAPI types).
2. `src/client/rest-client.ts`: one method per endpoint. `request()` already handles the base URL, bearer
token, timeout, JSON parsing, response validation and status-to-`ApiError` mapping. If your API uses
another auth scheme (API key header, OAuth client credentials), change the headers there.
Then adjust the tools in `src/mcp/` to the questions your users ask. Things that matter in practice:
- Keep tool descriptions specific: what it returns, units (cents), and which tool to call first.
- Write a recovery hint for every 404 (`callApi(fn, "Use search_products to find a valid SKU")`).
- Keep writes behind `STORE_ALLOW_WRITES` and the dry run, and give the server an API key that can only
do what the tools need.
- Delete `src/api/` once you have a real API; tests can keep using it as a stand-in.
## Tests
```bash
npm run check # typecheck + eslint + vitest + build
```
32 tests. They start the real sample API on a random port and talk to the MCP server through the SDK's own
`Client`: over the in-memory transport, as a spawned stdio process, and over Streamable HTTP. Covered: every
tool, the resources, the prompt, input validation, dry run vs confirm (stock and orders checked in the
database afterwards), 401 / 404 / 409 / timeout / connection refused, HTTP token check, 405 on GET and the
Host-header check.
Screenshots of a `sales_summary` call and the test run: [docs/sales.png](docs/sales.png),
[docs/tests.png](docs/tests.png). They show the bundled sample data.
## Stack
TypeScript (strict), Node 22, `@modelcontextprotocol/sdk` 1.32, zod 4, Express 5, node:sqlite, vitest 5,
ESLint with typescript-eslint.
## License
MIT
TDQS
Scored across 6 tools
Each tool targets a distinct resource+action pair: product search vs single product fetch, low-stock filtering, single order fetch, per-customer order history, and aggregate sales reporting. The two order-reading tools (get_order vs list_customer_orders) are clearly separated by scope (one order vs a customer's profile plus all orders), and descriptions reinforce the boundaries.
Five of six names follow a clean verb_noun pattern (search_products, get_product, list_low_stock, get_order, list_customer_orders). sales_summary breaks the pattern by dropping the verb, a minor but noticeable deviation.
Six tools is a tight, well-scoped set with no filler and no overlap. It leans slightly thin for a commerce bridge, with no write-side tools at all, but every included tool earns its place.
Read coverage is solid (catalog search/detail, stock alerts, orders, customer history, sales aggregation), but the surface is entirely read-only: no product creation, no stock adjustment/restock, and no order status or fulfillment updates. Agents can answer questions but cannot complete inventory or order lifecycle operations.