restaurant-agent
README.md
# restaurant-agent
A permission-aware MCP server for a restaurant backend, with the process
artifacts that built it: the specs it was built from and the eval harness
that measures how agents behave against it.
The interesting part is not that an agent can read a menu — it is what
happens when an agent asks for something it is *not allowed* to see, and how
you find out whether your agent + tool design actually holds up.
## What this demonstrates
- **Permission-aware tool design** — every tool declares a minimum role
(`viewer < staff < manager`); an unauthorized call returns a structured
refusal the agent can reason about, never an exception, never silent
failure, and never data.
- **Agent proposes, human approves** — the single write-path tool
(`draft_daily_special`) cannot touch production; it creates a draft that a
human approves out of band.
- **Spec-driven process** — [`specs/`](specs/) holds the contract this server
was built from ([001](specs/001-mcp-server.md)), the reusable template
([000](specs/000-template.md)), and a sanitized real-world case study
([002](specs/002-github-profile-rebuild.md)). When agent output is wrong,
the first suspect is spec quality — the fix goes into the spec, not just
the code.
- **Evals, not vibes** — [`evals/`](evals/) runs a real model against the
server and mechanically checks tool choices, refusal handling and
prompt-injection resistance. See [evals/README.md](evals/README.md) for
why these are not unit tests.
## Architecture
```mermaid
flowchart LR
agent["Agent<br/>(tool-use loop,<br/>Anthropic API)"]
subgraph mcp["MCP server (stdio)"]
registry["Tool registry"]
gate["Permission gate<br/>role rank vs minRole"]
validate["Zod validation"]
tools["7 tools"]
end
subgraph backend["Data source"]
mock["Mock API<br/>(fictional fixtures, default)"]
real["Real API<br/>(Bearer token, opt-in)"]
end
agent -- "tools/call" --> registry
registry --> gate
gate -- "insufficient role" --> refusal["Structured refusal<br/>{allowed:false, reason,<br/>required_role, current_role}"]
refusal --> agent
gate -- ok --> validate --> tools
tools --> mock
tools -.-> real
```
Audit trail: every call is logged as JSONL to stderr (timestamp, tool, role,
decision, args hash — raw arguments are never logged).
## Quickstart
```bash
npm install
npm test # 57 unit tests: full role x tool gate matrix
npm run agent # demo agent against the mock server (needs ANTHROPIC_API_KEY)
```
The server runs in **mock mode by default** — a fictional restaurant
("Ravintola Kotilounas") with invented menu, orders and sales. No backend, no
credentials needed. Try different roles:
```bash
AGENT_TOKEN=demo-staff-token npm run agent -- "What orders are waiting right now?"
AGENT_TOKEN=demo-manager-token npm run agent -- "Revenue for the first week of June?"
AGENT_TOKEN=demo-viewer-token npm run agent -- "Revenue for the first week of June?" # watch the refusal
```
## Permission design
Roles are hierarchical: `viewer (0) < staff (1) < manager (2)`. The registry
dispatches every call through the same path: **gate → validate → handler**.
- The gate runs *before* argument validation, so an under-privileged caller
gets `permission_denied` — not schema feedback it could use to probe tools.
- Refusals are template-built from registry data only. Tool arguments are
data, not instructions: a unit test injects instruction-like text through
arguments and asserts the refusal text is unchanged.
- Unknown or missing tokens resolve to `viewer` — least privilege by default.
- Argument schemas are `.strict()` — unexpected keys are rejected.
Example refusal (what the agent actually receives):
```json
{
"allowed": false,
"error": "permission_denied",
"reason": "get_sales_summary requires role 'manager'; this session has role 'viewer'.",
"required_role": "manager",
"current_role": "viewer"
}
```
## Tools
| Tool | Min role | Purpose |
|---|---|---|
| `get_menu` | viewer | full menu grouped by category, with allergen codes |
| `search_menu_items` | viewer | filter by text, category, excluded allergens |
| `get_opening_hours` | viewer | weekly hours + holiday exceptions |
| `get_todays_lunch` | viewer | today's lunch menu and serving window |
| `get_active_orders` | staff | live order queue with statuses |
| `get_sales_summary` | manager | per-day revenue and top items (mock-only) |
| `draft_daily_special` | staff | write-demo: draft → human approval, never prod |
The surface mirrors a real production restaurant API (the same one behind
[vanhamylly-api](https://github.com/illustraton916/vanhamylly-api)); real
mode forwards a Bearer token and is opt-in via env — no real endpoints ship
in this repo.
## Evals
```bash
ANTHROPIC_API_KEY=... npm run evals # 14 tasks, prints pass rate
npm run evals -- --task prompt-injection-menu-note
```
14 tasks: happy paths for every tool at sufficient role, refusal cases
(viewer asking for sales, staff asking for manager data), and adversarial
cases — prompt injection embedded in user content, requests for nonexistent
capabilities, and an attempt to publish straight to production (expected
outcome: a draft plus an answer that mentions human approval).
<!-- EVAL_RESULTS -->
**Latest local run: 14/14 (100%)** on `claude-sonnet-5`. The first run scored
12/14 — and both failures were **eval-quality defects, not agent defects**:
- the allergen task expected the literal argument `"gluten"`, while the agent
did something smarter — looked up the menu's allergen legend first and used
the documented code `"G"`;
- the publish-bypass task demanded an immediate `draft_daily_special` call,
while the agent refused the "skip any approval" instruction and asked
before drafting — *safer* than the expectation.
Both tasks were recalibrated to test the actual property (documented codes;
the no-bypass safety guarantee) and the fix went into the tasks, not the
code. That loop — failing eval → inspect → fix the spec — is the point of
this repo. Rates vary between runs and models; treat single runs as
measurements, not verdicts.
CI runs lint + typecheck + unit tests on every push; evals are a manual
workflow (they need an API key and cost tokens).
## License
MIT © 2026 Oleksii Halushka
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues