business-tools
by BelAnas
README.md
# MCP server for business tools
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets Claude and other AI
assistants work with a company's order system: look up customers, orders and stock, and take real
actions such as creating an order or requesting a refund.
It is for teams that want an assistant to do routine operations work but cannot hand a language
model a database password. Every API key has a role, the business rules are enforced in code, and
every call is written to an audit log that shows if anyone edits it afterwards.
## What it does
- Offers 13 tools over MCP: six that read (customers, products and stock, orders, sales figures,
the audit log) and seven that act (customer note, create order, change order status, request and
decide refunds, adjust stock).
- Works over both MCP transports: stdio for desktop assistants, and HTTP with a bearer token for
remote or shared use.
- Gives each API key one of three roles. A key sees and can call only the tools of its role.
- Masks customer email and phone for roles that may not read them.
- Enforces the business rules in code: an order moves only along allowed steps, refunds never
exceed what was paid, large refunds wait for a manager, nobody decides their own refund.
- Writes one audit record per call, allowed or refused. Records are chained by hash, so an edited
or deleted record is detected.
- Accepts an idempotency key on every action, so a retried call does not act twice.
- Returns errors as structured results with a code and a plain message, so the assistant can
correct itself.
- Includes a demo agent and an evaluation script. Everything runs with no API key.
## How it works
```mermaid
flowchart TD
A[AI assistant<br/>Claude Desktop, Claude Code,<br/>or the demo agent] -->|MCP over stdio<br/>key in the environment| S
A2[Remote assistant or service] -->|MCP over HTTP<br/>Authorization: Bearer key| B[Bearer check<br/>401 without a valid key]
B --> S[MCP server<br/>server.py]
S -->|tools/list| F[Tool list filtered by role]
S -->|tools/call| G
subgraph G[Gateway: the single path for every call]
direction TB
G1[1. Rate limit per key] --> G2[2. Permission of the role]
G2 --> G3[3. Arguments validated<br/>with Pydantic]
G3 --> G4[4. Idempotency key<br/>already used?]
G4 --> G5[5. Tool runs in one transaction<br/>business rules first]
end
G5 --> D[(SQLite<br/>customers, products,<br/>orders, refunds)]
G -->|every call: ok, denied or error| L[(Audit log<br/>hash-chained)]
G -->|result, or error with code and message| A
```
The MCP layer (`server.py`) only translates protocol messages. All rules live in the gateway
(`gateway.py`) and the tools, which is why they are tested both directly and through MCP.
### Tools and roles
Output of `python -m business_mcp tools`:
```
tool kind permission viewer agent manager
search_customers read customers:read yes yes yes
get_customer read customers:read yes yes yes
search_products read products:read yes yes yes
get_order read orders:read yes yes yes
list_orders read orders:read yes yes yes
sales_summary read reports:read yes yes yes
read_audit_log read audit:read - - yes
add_customer_note action customers:write - yes yes
create_order action orders:write - yes yes
update_order_status action orders:write - yes yes
request_refund action refunds:request - yes yes
decide_refund action refunds:approve - - yes
adjust_stock action inventory:write - - yes
```
The server also publishes two resources (`business://policies`, generated from the same tables
the gateway enforces, and `business://schema`) and two prompts (`daily_briefing`, `review_refund`).
## Real output
The demo agent asks for a refund above the agent's limit. The server stores it as pending and
tells the assistant what to say:
```
$ python -m business_mcp demo "Refund \$300 on order O00012 because the wrong items were sent"
Connected to business-tools over stdio. 10 tools offered to this key. Provider: mock.
Task: Refund $300 on order O00012 because the wrong items were sent
1. get_order({"order_id": "O00012"})
-> {"id": "O00012", "customer_id": "C0011", "status": "delivered", "total": 453.1, "created_at": "2026-04-10T13:14:00Z", "customer_name": "Jonas Petrov", "company": "Granite Bookshop", "lines": [{"product_id": "P029", "name...
2. request_refund({"order_id": "O00012", "amount": 300.0, "reason": "the wrong items were sent"})
-> {"refund_id": "R0001", "order_id": "O00012", "amount": 300.0, "status": "pending", "message": "Waiting for a manager: the amount is above your limit of 100. Tell the customer the refund is under review, not that it is do...
Answer: Refund R0001 of $300.00 on order O00012 is pending. Waiting for a manager: the amount is above your limit of 100. Tell the customer the refund is under review, not that it is done.
```
The same kind of request with a read-only key. The action tool is not even offered:
```
$ python -m business_mcp demo "Mark order O00300 as shipped" --key demo-viewer-key
Connected to business-tools over stdio. 6 tools offered to this key. Provider: mock.
Task: Mark order O00300 as shipped
Answer: I cannot do that with my access: the tool update_order_status is not available to this API key.
```
The audit log afterwards:
```
$ python -m business_mcp audit tail -n 5
1 2026-10-12T12:07:04Z demo-agent stdio get_order ok order O00012
2 2026-10-12T12:07:04Z demo-agent stdio request_refund ok refund R0001 pending, 300.0
$ python -m business_mcp audit verify
Audit log intact: 2 records. Head hash: 495d91a08606c80c436d0eddf4ce6e646fbb0dc9aed88685074285639d4eb4c2
```
## Results
All numbers below come from commands run for this repository on one 8-core CPU machine.
| What was measured | Result | Command |
|---|---|---|
| Automated tests, offline, no key | 46 passed | `pytest -q` |
| Role and tool combinations behaving as the written policy says (3 roles x 13 tools, over stdio) | 39 of 39 | `python -m evals.run_eval` |
| Tool list offered to each role matches the policy | 3 of 3 roles | `python -m evals.run_eval` |
| Guardrail cases handled correctly (forbidden moves, bad arguments, masking, retry, rate limit) | 29 of 29 | `python -m evals.run_eval` |
| Refused calls that changed any business data | 0 of 25 | `python -m evals.run_eval` |
| HTTP authentication cases (no key, unknown key, valid key, revoked key) | 4 of 4 | `python -m evals.run_eval` |
| Audit records compared with calls counted by the client | 893 records for 893 calls | `python -m evals.run_eval` |
| Tampering detected (edited record, deleted record, edited record with its hash recomputed) | 3 of 3 | `python -m evals.run_eval` |
| Read call over stdio, median / 95th percentile (300 calls) | 6.0 ms / 8.8 ms | `python -m evals.run_eval` |
| Action call over stdio, median / 95th percentile (100 calls) | 5.1 ms / 7.0 ms | `python -m evals.run_eval` |
| Read call over HTTP, median / 95th percentile (300 calls) | 13.0 ms / 17.1 ms | `python -m evals.run_eval` |
| Action call over HTTP, median / 95th percentile (100 calls) | 12.0 ms / 14.7 ms | `python -m evals.run_eval` |
| Demo agent tasks on the offline scripted provider | 10 of 10 | `python -m evals.run_eval` |
The offline provider is a script that recognises a fixed set of phrasings. Its 10 of 10 shows that
the client, the protocol and the server work together; it says nothing about how well a language
model chooses tools.
**Real-model check: pending.** No OpenAI key was available when this was built, and the local
model that was tried did not answer in time, so the demo agent has not yet been run with a real
language model and nothing was spent. What is covered instead: a test runs the real OpenAI client
library against a local stub server and confirms that the tool definitions and the token cap are
sent and that a tool call and a final answer are read back correctly. `python -m evals.live_check`
is ready to run the eight live tasks as soon as a key or a local model is set.
The evaluation fails when it should: with the `orders:write` permission deliberately added to the
viewer role, the same command reported 37 of 39 and exited with an error.
## Quick start
No API key is needed. Python 3.12.
```bash
git clone https://github.com/BelAnas/mcp-business-tools-server.git
cd mcp-business-tools-server
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest -q # the test suite
python -m business_mcp demo "What is the status of order O00012?"
python -m evals.run_eval # the measurements in the table above
```
The first command that needs the database creates `data/business.db`: a fictional office-supplies
wholesaler with 60 customers, 40 products and 300 orders, generated from a fixed seed, plus three
demo keys (`demo-viewer-key`, `demo-agent-key`, `demo-manager-key`).
Other requests the offline demo understands:
```bash
python -m business_mcp demo "Which products are running low?"
python -m business_mcp demo "Show sales from 2026-09-01 to 2026-09-30 by category"
python -m business_mcp demo "Add a note to customer C0005: asked for a paper quote"
python -m business_mcp demo "Mark order O00012 as cancelled" # refused: a delivered order is final
```
### Connect an assistant
Claude Desktop (`claude_desktop_config.json`), with the paths of your clone:
```json
{
"mcpServers": {
"business-tools": {
"command": "/path/to/mcp-business-tools-server/.venv/bin/python",
"args": ["-m", "business_mcp", "serve"],
"cwd": "/path/to/mcp-business-tools-server",
"env": { "BUSINESS_MCP_API_KEY": "demo-agent-key" }
}
}
}
```
Claude Code:
```bash
claude mcp add business-tools --env BUSINESS_MCP_API_KEY=demo-agent-key \
-- /path/to/mcp-business-tools-server/.venv/bin/python -m business_mcp --db /path/to/mcp-business-tools-server/data/business.db serve
```
The role of the key decides what the assistant can do. Start with `demo-viewer-key` to try it
read-only.
### Run over HTTP
```bash
python -m business_mcp serve --transport http # http://127.0.0.1:8931/mcp
curl -s -X POST http://127.0.0.1:8931/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# {"error": "A valid API key is required: Authorization: Bearer <key>"}
```
### Use a real model in the demo agent
```bash
export LLM_PROVIDER=openai OPENAI_API_KEY=... # or OPENAI_BASE_URL for a local model
python -m business_mcp demo "Cancel order O00298 and tell me what happened to the stock"
python -m evals.live_check # 8 tasks judged by the database
```
### Real keys
```bash
python -m business_mcp keys create --label support-bot --role agent # printed once, stored as a hash
python -m business_mcp keys list
python -m business_mcp keys revoke --label support-bot
```
Before any real use, delete the demo keys (`keys revoke --label demo-agent`, and so on).
## Configuration
Copy `.env.example` to `.env`. Environment variables take precedence over the file.
| Setting | Default | Meaning |
|---|---|---|
| `BUSINESS_MCP_API_KEY` | empty | The key the server runs as over stdio. It refuses to start without a valid one. |
| `BUSINESS_MCP_DB` | `data/business.db` | Database file. |
| `BUSINESS_MCP_HOST`, `BUSINESS_MCP_PORT` | `127.0.0.1`, `8931` | Address of the HTTP transport. |
| `RATE_LIMIT_PER_MINUTE` | `120` | Calls allowed per key per minute. |
| `AGENT_REFUND_LIMIT` | `100` | Largest refund an agent key gets approved at once. |
| `MANAGER_REFUND_LIMIT` | `1000` | Largest refund a manager key gets approved at once. |
| `LLM_PROVIDER` | `mock` | Demo agent only: `mock` (offline script) or `openai`. |
| `OPENAI_API_KEY`, `OPENAI_MODEL`, `OPENAI_BASE_URL` | empty, `gpt-4o-mini`, empty | Demo agent only. Set the base URL for Ollama or another compatible server. |
| `MAX_OUTPUT_TOKENS` | `300` | Demo agent only: cap on each model reply. |
## Project structure
```
business_mcp/
config.py settings read from the environment
db.py SQLite schema and connections (read tools get a read-only connection)
seed.py the synthetic wholesaler, generated from a fixed seed
auth.py API keys (hashed), roles and their permissions
errors.py the error codes tools return
ratelimit.py per-key call limit
audit.py hash-chained audit log and its verification
gateway.py the single path for every call: checks, transaction, audit
policy.py the policy text served as an MCP resource, generated from the code
server.py the MCP server: stdio, HTTP with bearer check, resources, prompts
cli.py serve, seed, keys, audit, tools, demo
tools/
registry.py one entry per tool: argument model, permission, handler
read.py the six read tools
write.py the seven action tools and the business rules
agent/
providers.py offline scripted provider and OpenAI-compatible provider
loop.py the agent loop over an MCP client
run.py the demo command
evals/
run_eval.py permission matrix, guardrails, audit, HTTP auth, latency, agent tasks
live_check.py the demo agent with a real model, judged by the database
report.json the full report of the last evaluation run
tests/ pytest suite: gateway rules, MCP over three transports, agent
```
## Limits
- The business system is a demo SQLite database with synthetic data. A real deployment replaces
the SQL in the tools with calls to the company's own system; the gateway stays the same.
- Authentication is by static API key. There is no OAuth sign-in and no per-user identity behind
a key: the audit log names the key, not the person using the assistant.
- The audit chain detects edits and deletions inside the log. It cannot detect the removal of the
most recent records unless the head hash (printed by `audit verify`) is also stored somewhere
the server cannot write to. That export is not automated here.
- The audit log stores the arguments of each call, capped at 1,000 characters. Free text such as
a note or a search term is therefore copied into the log.
- The rate limit is kept in memory per server process. Several processes behind a load balancer
would each count separately.
- SQLite allows one writer at a time. The response times were measured with one client making
calls one after another, not under concurrent load.
- Amounts have no currency and no tax. A refund is a record with a status; no payment provider
is called.
- The offline provider of the demo agent is a script for a fixed set of phrasings, not a model.
- Claude Desktop and Claude Code were not driven by hand in this build. Compatibility was tested
with the official MCP client library, with both the current protocol and the older handshake.
- The HTTP transport has no TLS of its own; it is meant to sit behind a reverse proxy.
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues