Business Operations MCP Server
Click on "Install 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., "@Business Operations MCP ServerCan you cater 80 people this Saturday? We'd want delivery."
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.
Business Operations MCP Server
An MCP server that exposes a small food business's operations — catalog, booking capacity, stock, catering quotes, and orders — as tools any MCP client can call.
Built with the Model Context Protocol Python SDK
(mcp 2.0). The tools are defined once and work in Claude Desktop, Cursor, or this
project's own agent, without being rewritten per framework. That reusability is the
argument for the protocol, so the server is the artifact and the agent is one of
its clients.
A LangGraph agent ships with it as one client, so you can see the tools drive a real multi-step conversation.
It is live. The server runs on AWS Lambda behind a public Function URL, so you can call it without cloning anything:
curl -s -X POST https://w2f7mj2jcbr3jberiepx2iu2nu0xpgcm.lambda-url.us-east-1.on.aws/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Point an MCP client at that URL and the six tools show up. See Deployed on AWS for how it gets there.
Quick start
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
python server.py --seed # build the demo database
python server.py # stdio transport
python server.py --http # streamable HTTP on :8000/mcpUse it from Claude Desktop
{
"mcpServers": {
"business-ops": {
"command": "/absolute/path/to/.venv/bin/python",
"args": ["/absolute/path/to/server.py"]
}
}
}Related MCP server: Caramelo MCP Server
The tools
Tool | Reads / writes | What it does |
| read | Products and unit prices, filterable by category |
| read | Remaining booking capacity for a date |
| read | On-hand quantities, flagging items below reorder level |
| read | Prices a request and checks every booking rule |
| write | Books an order and consumes the day's capacity |
| read | Retrieves a booked order |
Plus one resource, ops://policy — the booking rules as Markdown. It is reference
text rather than an action, which is exactly the distinction MCP resources exist for.
Read-only tools carry readOnlyHint, so a client can auto-approve them and prompt
only for place_order — the one call that changes the business's schedule.
The agent
cp .env.example .env # add a free GROQ_API_KEY
python -m agent.cli "Can you do catering for 80 people this Saturday? We'd want delivery."
python -m agent.cli --trace # same, but print every tool call and result
python -m agent.cli # interactiveA LangGraph ReAct loop (Groq, openai/gpt-oss-120b) over whatever tools the server
advertises. It contains no business rules — it does not know the catering
minimum, because quote_catering tells it. Add a tool to the server and the agent
can use it with no change on the agent side; that property is the reason to build a
server rather than a bag of framework-specific functions.
Not langchain-mcp-adapters
That package is the obvious dependency and was the first choice. Version 0.3.2 pins
mcp<2, so installing it downgrades the SDK from 2.0.0 to 1.29.0 — and this server
is written against the 2.0 API (MCPServer, which does not exist in 1.x). Taking the
adapter would mean rewriting a working, tested server against an older SDK to satisfy
a client-side convenience wrapper.
agent/bridge.py is the alternative, and it is short because MCP is a good protocol:
a tool listing already carries a name, a description and a JSON Schema, and
langchain-core accepts a raw JSON Schema as args_schema. The server's schema
passes straight through — there is no hand-maintained pydantic mirror of each tool to
drift out of sync. When the adapter supports mcp 2.x, the dependency becomes the
better answer and that file should go.
Three agent bugs that only appeared when it ran
Unit tests cover the tools. None of these would have failed one.
Symptom | Fix |
Asked about 80 people on a day with 70 left, it reported the request was "5 servings short". It is 10. | Prompt: report the two numbers, never compute the difference |
It priced a 70-person alternative from memory instead of calling the tool. The figures happened to be right — this time. | Prompt: never price an option you have not looked up; suggest it and offer to check |
It called Sunday 2026‑08‑16 "Saturday" while every figure in the same answer was correct. | Tools: return a |
The third is the interesting one, because the fix was not in the prompt. A model that
has to derive a value will sometimes derive it wrong, confidently, in the middle of
an otherwise correct answer. Returning weekday next to day costs nothing and
removes the whole class — the same reasoning as returning total_cents next to
total so it never does mental arithmetic on money. Where a tool can hand over a
derived value, that beats instructing the model not to get it wrong.
Each has a regression test in tests/test_agent.py.
Trajectory evaluation
python -m evals.run_eval # 12 scenarios
python -m evals.run_eval --only spanish --repeat 6 # measure a flaky one
python -m evals.run_eval --json out.jsonAnswer-level scoring is not enough for an agent. A reply can be right by luck — priced from memory, correct that time — and wrong in a way no reader notices, like a confident weekday that does not match the date. Both happened here, and neither would fail a final-answer check.
So the harness scores the trajectory: which tools were called, in what order, with which arguments, and whether every figure in the answer traces back to something a tool returned.
Check | Catches |
| Answering a question about live data with zero tool calls |
| Skipping a lookup the answer depends on |
| Writing to the schedule when only asked a question |
|
|
| Resolving "this Saturday" to the wrong date, wrong headcount |
| A price no tool ever returned |
| A computed figure — the "5 servings short" bug |
| Reporting a closed day as "fully booked" |
The grounding checks are the interesting ones, and they need no judge model. Pull every figure out of the answer; check it appears in a tool result, in the question, or in the arguments the agent itself sent. Deterministic, free, no drift — and it stays correct when an invented number happens to be right, because the question is whether the agent looked it up.
Results
12 scenarios, openai/gpt-oss-120b: 11/12 passed, mean score 0.98.
The one failure was real and worth the whole exercise. Asked "Are you on Uber Eats or DoorDash?" the agent replied "I don't have that information available in our system" with zero tool calls — the same class of bug as the sibling RAG project's off-topic refusal. The prompt already forbade refusing as off-topic; the model did not read that as covering a claim of ignorance, so the rule now names it directly: not knowing is a conclusion you may only reach after looking.
It is also intermittent — the same scenario passed on the previous run. That is
why --repeat exists: a single pass measures a sample of one, and flaky agent
behaviour is exactly what gets written down as "works" after one lucky run.
Honest gap: the post-fix rate on that scenario is unmeasured. The Groq free tier has a 200k-token daily cap and this session hit it. The fix has a regression test on the prompt text; the behavioural confirmation is still owed.
The scorer was wrong four times before it was right
Worth stating plainly, because an eval that is wrong is worse than no eval — it produces numbers that look authoritative and are not. Every one of these marked a correct agent answer as a fabrication:
False positive | Cause |
| Models write dates with typographic hyphens ( |
| A lookahead rejecting a following comma skipped every number in a JSON result ( |
| "el sábado 19 de septiembre" repeats a date the tool returned; stripping ISO dates does not cover long-form renderings |
| A list marker is presentation, not a quantity |
Each has a regression test in tests/test_evals.py. The first draft of this harness
reported 2/12 passing; almost all of that was the scorer, not the agent.
Live demo
streamlit run app.pyThe agent with its tool trajectory rendered next to every answer — which tools it reached for, with what arguments, and what came back. A chat window that answers correctly proves nothing about an agent; the trajectory is the demonstration.
Self-contained on purpose: SQLite, no external database, no embedding model, one API key. A demo whose database can pause is a demo that is dead exactly when someone opens it.
Each visitor gets their own copy of the business. place_order permanently
consumes a day's capacity, so on a single shared database every booking a visitor
makes leaves less room for the next, and after a handful of visits the demo answers
"no availability" to everything and looks broken. A per-session SQLite file in the
temp directory fixes that, and removes any assumption that the deploy's working
directory is writable.
Failures are unwrapped before they are classified. MCP and LangGraph run tool calls
inside anyio task groups, so a rate limit arrives as an ExceptionGroup whose own
message says nothing about rate limits — matching on the outer str(exc) showed a
plain "try again shortly" as an unexplained crash on the live demo, and stopped the
retry logic from firing at all. Both now walk the whole tree.
Before deploying, the app is booted from a clean install of requirements.txt
alone and executed headlessly with Streamlit's AppTest — "the server starts" is
not the same as "the script runs", and a missing dependency shows up on the first
browser connection, which is exactly when someone is looking.
To deploy on Streamlit Community Cloud, point it at app.py and set one secret:
GROQ_API_KEY = "gsk_..."Three decisions worth explaining
Every blocker is returned at once, not the first one
A catering request can fail four ways simultaneously: too little notice, under the
minimum, over the day's capacity, and short on stock. quote_catering checks all of
them and returns the full list.
Short-circuiting on the first failure would cost a round trip per problem — fix the lead time, call again, discover the headcount is too low, call again — and it reads to the customer like being told the bad news one piece at a time.
A blocked quote is still fully priced, so the customer can see what the order would cost if the blocker were resolved.
"Closed" and "fully booked" are different answers
A Monday has no capacity row at all; a booked-out Saturday has a row with no
headroom left. The distinction survives from the schema up to the tool response,
because the two send the customer in opposite directions: one means pick another
date, the other means the date works if the headcount changes. Collapsing both into
"unavailable" is the kind of small lie that wastes a customer's afternoon.
A test (test_seeding_leaves_mondays_absent_rather_than_at_zero_capacity) pins this,
because a Monday row with max_servings = 0 would read as "full" to every layer
above it.
Money is integer cents, everywhere
Every amount is an int number of cents; formatting to $9.50 CAD happens once, at
the edge. Floats are never used for money — 0.1 + 0.2 != 0.3 in binary floating
point, and a quote that is off by a cent a line is off by real money by the time it
reaches a customer. Responses carry both forms (total and total_cents) so the
model has something to display and something to compute with.
Architecture
server.py MCP adapter — tool definitions and argument handling, nothing else
lambda_handler.py AWS entry point: the same server behind a Function URL
ops/
store.py The storage interface and the process-wide active backend
rules.py Booking rules and quote maths — pure functions, no I/O, no clock
money.py Integer-cent arithmetic
backends/
sqlite.py Local and demo: a file, or one per visitor session
dynamo.py Deployed: one single-table design, PK/SK
agent/
bridge.py MCP tools -> LangChain tools (see "Not langchain-mcp-adapters")
graph.py The ReAct agent and its operating instructions
cli.py Ask it things; --trace shows the tool trajectory
evals/
dataset.py 12 scenarios with the trajectory each should produce
trajectory.py Scoring — pure functions, no LLM, no network
score.py Applying a scenario's expectations to a run
run_eval.py The runner
infra/ Terraform for the AWS deployment, and deploy.sh
app.py Streamlit demo, tool trajectory shown per answer
tests/ 161 tests, no network and no LLM requiredops has no MCP import and rules has no database import. The decision to accept an
order is therefore testable without starting a transport, and the clock is passed in
rather than read, so the suite gives the same answer in January as in July.
The one place that intentionally reaches across the layers is store.record_order,
which writes the order and consumes the day's capacity in a single transaction.
Two separate writes would let the business double-book a Saturday whenever the second
one failed; test_a_refused_booking_leaves_no_order_behind guards it.
Deployed on AWS
The server runs behind a public Lambda Function URL, so it is a remote MCP server — a URL a client connects to, not a repo someone has to clone and run.
push to main
│
┌───────▼────────┐
│ GitHub Actions │ pytest (moto) ─── gate
│ OIDC, no key │ docker build ── push ── update code ── smoke test
└───────┬────────┘
│ short-lived credentials, refs/heads/main only
══════════════════ │ ═══════════════════════ AWS ══════════════════════
▼
MCP client ──► Function URL ──► Lambda (container, 512 MB)
(Claude Desktop, auth NONE lambda_handler.py
curl, the agent) │ Mangum ── ASGI ── MCP streamable HTTP
│ stateless, JSON responses
▼
DynamoDB business-ops
on-demand, PK/SK single table
▲
│
CloudWatch Logs (14-day retention)
ECR mcp-business-agent ── lifecycle: 3 images, untagged expire in a dayops.store is an interface with two backends, so nothing above it changes
between the SQLite demo and the deployed table. rules.py does not know either
exists.
Deploying
cd infra
./bootstrap-state.sh # once per account: the S3 bucket holding Terraform state
./deploy.sh # build, push, apply, smoke test
./deploy.sh --seed # ...and repopulate the business (destructive)State lives in S3, not on a laptop. Local state means the record of what exists in AWS has exactly one copy: lose it and Terraform knows about none of the resources it created, so the next apply tries to build a second copy of everything, fails on the names already taken, and leaves the originals running and unmanaged. The bucket is versioned and bootstrapped by hand, because a configuration cannot create the bucket its own backend needs in order to run.
Two ordering constraints shape that script, and neither is obvious from the Terraform alone.
A container-image Lambda cannot be created before the image exists, and the
image cannot be pushed before the registry does — so the registry is applied on
its own first. Then the Function URL's hostname is only known after the function
exists, and the function needs that hostname in ALLOWED_HOSTS or MCP's DNS
rebinding protection answers every request with 421. A resource cannot depend on
an attribute of something that depends on it, so the final apply feeds this
configuration's own output back in as a variable. Terraform still owns the
value, and there is nothing out-of-band for a later apply to revert.
ALLOWED_HOSTS unset means the transport rejects everything rather than
accepting any Host, so a half-finished deploy fails closed.
What only the real deploy could catch
The image was run under the AWS Runtime Interface Emulator against the real table before any of this — which the 161 tests against moto cannot do. It proved the image starts, the handler works under the actual runtime, and boto3 reaches DynamoDB.
It still was not enough. CreateFunction failed with "The image manifest,
config or layer media type for the source image ... is not supported", which
reads like a base image problem and is not one. Recent BuildKit attaches
provenance and SBOM attestations by default, and to carry them it pushes an OCI
image index rather than a plain manifest; Lambda accepts only a single Docker v2
manifest. --provenance=false --sbom=false fixes it. Every pre-deploy check
passes while it is broken, because the fault is in what the registry stores, not
in the image.
CI/CD
Push to main runs the suite, builds, pushes, updates the function and smoke
tests it. Authentication is OIDC — GitHub presents a signed token naming the
repository and ref, AWS returns short-lived credentials, and there is no access
key in repository secrets.
The trust policy names refs/heads/main rather than repo:owner/name:*. The
wildcard also matches pull_request runs, and a pull request can come from a
fork, so the broad form lets anyone on GitHub open a PR that assumes the deploy
role.
The role also cannot run terraform apply. Applying this configuration
creates IAM roles, and a role that can create roles can grant itself anything —
not a capability to hand a public repository's OIDC trust. It can push an image
and repoint the function; infrastructure changes are applied from a workstation.
The smoke test sends three requests, not one, for the same reason the test suite does: Mangum re-runs the ASGI lifespan on every invocation and MCP's session manager refuses to start twice, so that failure only appears from the second request into a warm container.
It also fetches / in a browser's shape. That URL returned a bare 502 for a
while and nothing failed, because the protocol was healthy and no check looked:
MCP treats GET as a request to open an SSE stream and a Function URL in buffered
mode cannot stream one. A small ASGI wrapper now answers GET on / with a page
explaining what the endpoint is, and GET on /mcp with a 405 saying to POST.
Both are the difference between "this speaks a protocol" and "this is broken",
to a reader who cannot tell the two apart.
Cost
Free tier covers it: a million Lambda requests a month, 25 GB of DynamoDB. The only real exposure is ECR storage beyond the first 500 MB and CloudWatch logs kept forever, so the lifecycle policy holds three images and log retention is 14 days. A $5 budget alarm is the backstop, and the Function URL is unauthenticated by design — the seeded business is fictional and disclosable, but the invocations are still billable.
Tests
pytest -q # 161 tests, ~7sNo API key, no network, no running server. Tool tests go through
server.call_tool(...) rather than calling the Python functions directly, so the
registered schema and result envelope are covered too — a tool that works when
called directly and fails over the protocol is still broken.
Roadmap
Confirm the no-lookup fix behaviourally (blocked on the Groq daily token cap)
More scenarios around multi-turn bookings, where the agent has to carry a quote across turns before writing
Licence
MIT.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityCmaintenanceMCP server for Spice Garden demo restaurant that enables querying orders, inventory, tables, and sales data from a SQLite database, with an optional OpenRouter-powered chat assistant.
- Flicense-qualityDmaintenanceMCP server that exposes tools to query DulceGestion business data (orders, stock, customers) and retrieve documentation via RAG, enabling Claude to assist with pastry shop management.
- Flicense-qualityBmaintenanceDemo MCP server that exposes order and customer data as read-only tools for AI assistants, simulating a business API or internal data source.
- Alicense-qualityCmaintenanceHosted MCP server to manage a restaurant menu from AI agents - 39 tools over the DuckHub API (menu, categories, modifiers, prices, images, translations, promotions, and orders).MIT
Related MCP Connectors
MCP server exposing the Backtest360 engine API as tools for AI agents.
MCP server for Clipkit — gives AI agents a video toolbox via the Clipkit schema.
Hosted MCP endpoint with realistic fake data for prototyping agents. 12 tools, no setup.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/pcbeingused333/mcp-business-agent'
If you have feedback or need assistance with the MCP directory API, please join our Discord server