{"id":"aa340271-d749-481a-81b3-284a88fe0785","slug":"mcp-business-tools-server","kind":"mcp","name":"mcp-business-tools-server","summary":"A Model Context Protocol server that lets Claude and other AI assistants look up customers, orders and stock and take real actions, with a role for every API key, hard business rules in code and a tam","owner":"BelAnas","repoUrl":"https://github.com/BelAnas/mcp-business-tools-server","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-27T11:13:27.000Z","addedAt":"2026-10-28T09:37:34.105Z","logoUrl":"https://avatars.githubusercontent.com/u/61690844?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","memory-knowledge","databases"],"tags":["ai-agents","ai-integration","audit-log","claude","guardrails","llm","pydantic","python"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-28T10:30:12.320Z","sample":false,"url":"https://conduits.dev/tools/mcp-business-tools-server","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# MCP server for business tools\n\nA [Model Context Protocol](https://modelcontextprotocol.io) server that lets Claude and other AI\nassistants work with a company's order system: look up customers, orders and stock, and take real\nactions such as creating an order or requesting a refund.\n\nIt is for teams that want an assistant to do routine operations work but cannot hand a language\nmodel a database password. Every API key has a role, the business rules are enforced in code, and\nevery call is written to an audit log that shows if anyone edits it afterwards.\n\n## What it does\n\n- Offers 13 tools over MCP: six that read (customers, products and stock, orders, sales figures,\n  the audit log) and seven that act (customer note, create order, change order status, request and\n  decide refunds, adjust stock).\n- Works over both MCP transports: stdio for desktop assistants, and HTTP with a bearer token for\n  remote or shared use.\n- Gives each API key one of three roles. A key sees and can call only the tools of its role.\n- Masks customer email and phone for roles that may not read them.\n- Enforces the business rules in code: an order moves only along allowed steps, refunds never\n  exceed what was paid, large refunds wait for a manager, nobody decides their own refund.\n- Writes one audit record per call, allowed or refused. Records are chained by hash, so an edited\n  or deleted record is detected.\n- Accepts an idempotency key on every action, so a retried call does not act twice.\n- Returns errors as structured results with a code and a plain message, so the assistant can\n  correct itself.\n- Includes a demo agent and an evaluation script. Everything runs with no API key.\n\n## How it works\n\n```mermaid\nflowchart TD\n    A[AI assistant<br/>Claude Desktop, Claude Code,<br/>or the demo agent] -->|MCP over stdio<br/>key in the environment| S\n    A2[Remote assistant or service] -->|MCP over HTTP<br/>Authorization: Bearer key| B[Bearer check<br/>401 without a valid key]\n    B --> S[MCP server<br/>server.py]\n    S -->|tools/list| F[Tool list filtered by role]\n    S -->|tools/call| G\n\n    subgraph G[Gateway: the single path for every call]\n        direction TB\n        G1[1. Rate limit per key] --> G2[2. Permission of the role]\n        G2 --> G3[3. Arguments validated<br/>with Pydantic]\n        G3 --> G4[4. Idempotency key<br/>already used?]\n        G4 --> G5[5. Tool runs in one transaction<br/>business rules first]\n    end\n\n    G5 --> D[(SQLite<br/>customers, products,<br/>orders, refunds)]\n    G -->|every call: ok, denied or error| L[(Audit log<br/>hash-chained)]\n    G -->|result, or error with code and message| A\n```\n\nThe MCP layer (`server.py`) only translates protocol messages. All rules live in the gateway\n(`gateway.py`) and the tools, which is why they are tested both directly and through MCP.\n\n### Tools and roles\n\nOutput of `python -m business_mcp tools`:\n\n```\ntool                  kind    permission        viewer   agent    manager\nsearch_customers      read    customers:read    yes      yes      yes\nget_customer          read    customers:read    yes      yes      yes\nsearch_products       read    products:read     yes      yes      yes\nget_order             read    orders:read       yes      yes      yes\nlist_orders           read    orders:read       yes      yes      yes\nsales_summary         read    reports:read      yes      yes      yes\nread_audit_log        read    audit:read        -        -        yes\nadd_customer_note     action  customers:write   -        yes      yes\ncreate_order          action  orders:write      -        yes      yes\nupdate_order_status   action  orders:write      -        yes      yes\nrequest_refund        action  refunds:request   -        yes      yes\ndecide_refund         action  refunds:approve   -        -        yes\nadjust_stock          action  inventory:write   -        -        yes\n```\n\nThe server also publishes two resources (`business://policies`, generated from the same tables\nthe gateway enforces, and `business://schema`) and two prompts (`daily_briefing`, `review_refund`).\n\n## Real output\n\nThe demo agent asks for a refund above the agent's limit. The server stores it as pending and\ntells the assistant what to say:\n\n```\n$ python -m business_mcp demo \"Refund \\$300 on order O00012 because the wrong items were sent\"\nConnected to business-tools over stdio. 10 tools offered to this key. Provider: mock.\nTask: Refund $300 on order O00012 because the wrong items were sent\n  1. get_order({\"order_id\": \"O00012\"})\n     -> {\"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...\n  2. request_refund({\"order_id\": \"O00012\", \"amount\": 300.0, \"reason\": \"the wrong items were sent\"})\n     -> {\"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...\nAnswer: 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.\n```\n\nThe same kind of request with a read-only key. The action tool is not even offered:\n\n```\n$ python -m business_mcp demo \"Mark order O00300 as shipped\" --key demo-viewer-key\nConnected to business-tools over stdio. 6 tools offered to this key. Provider: mock.\nTask: Mark order O00300 as shipped\nAnswer: I cannot do that with my access: the tool update_order_status is not available to this API key.\n```\n\nThe audit log afterwards:\n\n```\n$ python -m business_mcp audit tail -n 5\n    1 2026-10-12T12:07:04Z demo-agent     stdio  get_order            ok  order O00012\n    2 2026-10-12T12:07:04Z demo-agent     stdio  request_refund       ok  refund R0001 pending, 300.0\n$ python -m business_mcp audit verify\nAudit log intact: 2 records. Head hash: 495d91a08606c80c436d0eddf4ce6e646fbb0dc9aed88685074285639d4eb4c2\n```\n\n## Results\n\nAll numbers below come from commands run for this repository on one 8-core CPU machine.\n\n| What was measured | Result | Command |\n|---|---|---|\n| Automated tests, offline, no key | 46 passed | `pytest -q` |\n| 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` |\n| Tool list offered to each role matches the policy | 3 of 3 roles | `python -m evals.run_eval` |\n| Guardrail cases handled correctly (forbidden moves, bad arguments, masking, retry, rate limit) | 29 of 29 | `python -m evals.run_eval` |\n| Refused calls that changed any business data | 0 of 25 | `python -m evals.run_eval` |\n| HTTP authentication cases (no key, unknown key, valid key, revoked key) | 4 of 4 | `python -m evals.run_eval` |\n| Audit records compared with calls counted by the client | 893 records for 893 calls | `python -m evals.run_eval` |\n| Tampering detected (edited record, deleted record, edited record with its hash recomputed) | 3 of 3 | `python -m evals.run_eval` |\n| Read call over stdio, median / 95th percentile (300 calls) | 6.0 ms / 8.8 ms | `python -m evals.run_eval` |\n| Action call over stdio, median / 95th percentile (100 calls) | 5.1 ms / 7.0 ms | `python -m evals.run_eval` |\n| Read call over HTTP, median / 95th percentile (300 calls) | 13.0 ms / 17.1 ms | `python -m evals.run_eval` |\n| Action call over HTTP, median / 95th percentile (100 calls) | 12.0 ms / 14.7 ms | `python -m evals.run_eval` |\n| Demo agent tasks on the offline scripted provider | 10 of 10 | `python -m evals.run_eval` |\n\nThe offline provider is a script that recognises a fixed set of phrasings. Its 10 of 10 shows that\nthe client, the protocol and the server work together; it says nothing about how well a language\nmodel chooses tools.\n\n**Real-model check: pending.** No OpenAI key was available when this was built, and the local\nmodel that was tried did not answer in time, so the demo agent has not yet been run with a real\nlanguage model and nothing was spent. What is covered instead: a test runs the real OpenAI client\nlibrary against a local stub server and confirms that the tool definitions and the token cap are\nsent and that a tool call and a final answer are read back correctly. `python -m evals.live_check`\nis ready to run the eight live tasks as soon as a key or a local model is set.\n\nThe evaluation fails when it should: with the `orders:write` permission deliberately added to the\nviewer role, the same command reported 37 of 39 and exited with an error.\n\n## Quick start\n\nNo API key is needed. Python 3.12.\n\n```bash\ngit clone https://github.com/BelAnas/mcp-business-tools-server.git\ncd mcp-business-tools-server\npython -m venv .venv && source .venv/bin/activate\npip install -r requirements.txt\n\npytest -q                                   # the test suite\npython -m business_mcp demo \"What is the status of order O00012?\"\npython -m evals.run_eval                    # the measurements in the table above\n```\n\nThe first command that needs the database creates `data/business.db`: a fictional office-supplies\nwholesaler with 60 customers, 40 products and 300 orders, generated from a fixed seed, plus three\ndemo keys (`demo-viewer-key`, `demo-agent-key`, `demo-manager-key`).\n\nOther requests the offline demo understands:\n\n```bash\npython -m business_mcp demo \"Which products are running low?\"\npython -m business_mcp demo \"Show sales from 2026-09-01 to 2026-09-30 by category\"\npython -m business_mcp demo \"Add a note to customer C0005: asked for a paper quote\"\npython -m business_mcp demo \"Mark order O00012 as cancelled\"     # refused: a delivered order is final\n```\n\n### Connect an assistant\n\nClaude Desktop (`claude_desktop_config.json`), with the paths of your clone:\n\n```json\n{\n  \"mcpServers\": {\n    \"business-tools\": {\n      \"command\": \"/path/to/mcp-business-tools-server/.venv/bin/python\",\n      \"args\": [\"-m\", \"business_mcp\", \"serve\"],\n      \"cwd\": \"/path/to/mcp-business-tools-server\",\n      \"env\": { \"BUSINESS_MCP_API_KEY\": \"demo-agent-key\" }\n    }\n  }\n}\n```\n\nClaude Code:\n\n```bash\nclaude mcp add business-tools --env BUSINESS_MCP_API_KEY=demo-agent-key \\\n  -- /path/to/mcp-business-tools-server/.venv/bin/python -m business_mcp --db /path/to/mcp-business-tools-server/data/business.db serve\n```\n\nThe role of the key decides what the assistant can do. Start with `demo-viewer-key` to try it\nread-only.\n\n### Run over HTTP\n\n```bash\npython -m business_mcp serve --transport http        # http://127.0.0.1:8931/mcp\ncurl -s -X POST http://127.0.0.1:8931/mcp -H 'Content-Type: application/json' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\n# {\"error\": \"A valid API key is required: Authorization: Bearer <key>\"}\n```\n\n### Use a real model in the demo agent\n\n```bash\nexport LLM_PROVIDER=openai OPENAI_API_KEY=...         # or OPENAI_BASE_URL for a local model\npython -m business_mcp demo \"Cancel order O00298 and tell me what happened to the stock\"\npython -m evals.live_check                            # 8 tasks judged by the database\n```\n\n### Real keys\n\n```bash\npython -m business_mcp keys create --label support-bot --role agent   # printed once, stored as a hash\npython -m business_mcp keys list\npython -m business_mcp keys revoke --label support-bot\n```\n\nBefore any real use, delete the demo keys (`keys revoke --label demo-agent`, and so on).\n\n## Configuration\n\nCopy `.env.example` to `.env`. Environment variables take precedence over the file.\n\n| Setting | Default | Meaning |\n|---|---|---|\n| `BUSINESS_MCP_API_KEY` | empty | The key the server runs as over stdio. It refuses to start without a valid one. |\n| `BUSINESS_MCP_DB` | `data/business.db` | Database file. |\n| `BUSINESS_MCP_HOST`, `BUSINESS_MCP_PORT` | `127.0.0.1`, `8931` | Address of the HTTP transport. |\n| `RATE_LIMIT_PER_MINUTE` | `120` | Calls allowed per key per minute. |\n| `AGENT_REFUND_LIMIT` | `100` | Largest refund an agent key gets approved at once. |\n| `MANAGER_REFUND_LIMIT` | `1000` | Largest refund a manager key gets approved at once. |\n| `LLM_PROVIDER` | `mock` | Demo agent only: `mock` (offline script) or `openai`. |\n| `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. |\n| `MAX_OUTPUT_TOKENS` | `300` | Demo agent only: cap on each model reply. |\n\n## Project structure\n\n```\nbusiness_mcp/\n  config.py        settings read from the environment\n  db.py            SQLite schema and connections (read tools get a read-only connection)\n  seed.py          the synthetic wholesaler, generated from a fixed seed\n  auth.py          API keys (hashed), roles and their permissions\n  errors.py        the error codes tools return\n  ratelimit.py     per-key call limit\n  audit.py         hash-chained audit log and its verification\n  gateway.py       the single path for every call: checks, transaction, audit\n  policy.py        the policy text served as an MCP resource, generated from the code\n  server.py        the MCP server: stdio, HTTP with bearer check, resources, prompts\n  cli.py           serve, seed, keys, audit, tools, demo\n  tools/\n    registry.py    one entry per tool: argument model, permission, handler\n    read.py        the six read tools\n    write.py       the seven action tools and the business rules\nagent/\n  providers.py     offline scripted provider and OpenAI-compatible provider\n  loop.py          the agent loop over an MCP client\n  run.py           the demo command\nevals/\n  run_eval.py      permission matrix, guardrails, audit, HTTP auth, latency, agent tasks\n  live_check.py    the demo agent with a real model, judged by the database\n  report.json      the full report of the last evaluation run\ntests/             pytest suite: gateway rules, MCP over three transports, agent\n```\n\n## Limits\n\n- The business system is a demo SQLite database with synthetic data. A real deployment replaces\n  the SQL in the tools with calls to the company's own system; the gateway stays the same.\n- Authentication is by static API key. There is no OAuth sign-in and no per-user identity behind\n  a key: the audit log names the key, not the person using the assistant.\n- The audit chain detects edits and deletions inside the log. It cannot detect the removal of the\n  most recent records unless the head hash (printed by `audit verify`) is also stored somewhere\n  the server cannot write to. That export is not automated here.\n- The audit log stores the arguments of each call, capped at 1,000 characters. Free text such as\n  a note or a search term is therefore copied into the log.\n- The rate limit is kept in memory per server process. Several processes behind a load balancer\n  would each count separately.\n- SQLite allows one writer at a time. The response times were measured with one client making\n  calls one after another, not under concurrent load.\n- Amounts have no currency and no tax. A refund is a record with a status; no payment provider\n  is called.\n- The offline provider of the demo agent is a script for a fixed set of phrasings, not a model.\n- Claude Desktop and Claude Code were not driven by hand in this build. Compatibility was tested\n  with the official MCP client library, with both the current protocol and the older handshake.\n- The HTTP transport has no TLS of its own; it is meant to sit behind a reverse proxy.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n"}