Skip to main content
Glama
BelAnas

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).