mcp-server-enterprise-tools
by sudsho
README.md
# mcp-server-enterprise-tools
An opinionated tool gateway for LLM-driven backends, designed around the
MCP tool model but shipped with its own JSON transports. It focuses on the
governance layer: per-tool RBAC from a declarative YAML policy, a
structured audit row for every invocation, PII redaction on arguments
before persistence, and a typed error envelope so clients can branch on
codes instead of string-matching messages.
The transport is a small HTTPS API (`POST /mcp/call`) plus a Prometheus
`/metrics` endpoint. The stdio path that would let a full MCP host
discover and call these tools is out of scope in this repo.
## The problem
Enterprise backends that want to expose internal APIs to LLM-driven
callers hit three gaps that are not addressed at the protocol layer:
- caller identity is not first-class and needs to be resolved out-of-band
- tool arguments and results can carry PII (SSNs, PANs, account numbers)
and those cannot land in cleartext in logs or audit tables
- error handling needs to be machine-readable so internal callers can
branch on codes without string-matching messages
This repo is an opinionated take on filling those gaps while keeping the
tool implementations small and readable.
## What is exposed
Three bank tools (four operations):
| Tool | Verb | Purpose |
|------|------|---------|
| `customer_360.lookup` | read | profile, primary account summary, risk flags |
| `statement_search.query` | read | full-text with date and amount filters |
| `dispute_resolution.open` | mutating | open a dispute against a transaction |
| `dispute_resolution.status` | read | dispute state by id |
Each has a JSON input schema. Adding a fourth is a single file under
`src/mcp_server/tools/` plus a line in `app.build_registry`.
## Architecture
```
+--------------------+ +------------------+
| Internal caller | | Internal agent |
| (any HTTP client) | | (http) |
+---------+----------+ +---------+--------+
| |
| X-Bank-Identity |
v v
+---+-------------------------------+---+
| Tool gateway (this repo) |
| transport -> registry.call -> handler |
| | |
| rbac.check + audit.write |
+----+-----------------+----------+------+
| | |
v v v
+------------------+ +---------+ +------------------+
| internal APIs | | Postgres| | Prometheus |
| (customer, | | audit | | metrics scraped |
| statements, | | log | | at /metrics |
| disputes) | +---------+ +------------------+
+------------------+
```
Longer version with request flow: [docs/architecture.md](docs/architecture.md).
## Quick start (runs offline, no keys)
No cloud creds, no api keys, no database to stand up. The audit log defaults
to a local sqlite file, and the three bank tools default to in-repo synthetic
upstreams (`MCP_UPSTREAM_MODE=fake`). Point `AUDIT_DB_URL` at postgres and set
`MCP_UPSTREAM_MODE=http` when you have the real services.
```bash
pip install -r requirements.txt
python -m examples.smoke # in-process server + client, exits 0
PYTHONPATH=src pytest -q # 31 passed
```
`python -m examples.smoke` starts the FastAPI gateway in-process, drives it
through the full governance path with the http client, and reads the audit
table back. Real output (trimmed):
```
1. client lists tools
- customer_360.lookup: Look up a customer 360 view: profile, primary account...
- statement_search.query: Search a customer's statements. Supports date range...
- dispute_resolution.open: Open a dispute against a specific transaction...
- dispute_resolution.status: Get the current status of an open dispute by id.
2. customer_360.lookup (allowed for svc-analyst-01)
{
"result": {
"profile": {
"name": "Jane A. Doe",
"email": "[REDACTED]<email>",
"ssn": "[REDACTED]<ssn>",
"notes": "primary card [REDACTED]<pan>on file; backup acct [REDACTED]<acct>"
},
...
}
}
5. dispute_resolution.open (DENIED for svc-analyst-01 by RBAC)
{
"error": {
"code": "forbidden",
"message": "identity 'svc-analyst-01' is not permitted to call 'dispute_resolution.open'",
...
}
}
6. audit log rows (read back from sqlite; args redacted)
[ok ] identity=svc-analyst-01 tool=customer_360.lookup error=-
args_redacted={"customer_id": "C-[REDACTED]<acct>"}
[ok ] identity=svc-dispute-agent-07 tool=dispute_resolution.open error=-
args_redacted={... "notes": "caller gave card [REDACTED]<pan>and ssn [REDACTED]<ssn>"}
[deny ] identity=svc-analyst-01 tool=dispute_resolution.open error=forbidden
4 audit rows written, no raw PAN/SSN present.
SMOKE OK
```
And `pytest`:
```
31 passed in 1.21s
```
## Quick start (production shape)
Local (http + postgres via compose):
```bash
docker compose up --build
# server on :8080, prometheus on :9090, postgres on :5432
curl -s -X POST http://localhost:8080/mcp/call \
-H "X-Bank-Identity: svc-analyst-01" \
-H "Content-Type: application/json" \
-d '{"tool":"customer_360.lookup","args":{"customer_id":"C-01234567"}}'
```
Bare local run against the http transport:
```bash
pip install -e '.[dev]'
export AUDIT_DB_URL=sqlite:///./audit.db # or a postgres url
make run-http # serves on :8080
```
## RBAC model
Deny by default. Policies in `configs/rbac.yaml` map identities to allowed
tool names, with a trailing `.*` wildcard for grouping. Every call, allowed
or denied, writes an audit row. Full spec in
[docs/rbac_model.md](docs/rbac_model.md).
## Audit log design
One row per tool call. It defaults to a local sqlite file so the server runs
offline, and swaps to Postgres for a real deploy by setting `AUDIT_DB_URL`
(the store is plain SQLAlchemy, so nothing else changes). Columns:
`request_id, identity, tool, outcome, args_redacted, error_code, created_at`.
Arguments are passed through `Redactor` before persistence: SSNs, PANs
(Luhn-checked), email addresses, and 8-17 digit account-shaped numbers
are replaced with marked tokens. The two read tools also apply the same
redactor to their upstream response as defense-in-depth. Details in
[src/mcp_server/redaction.py](src/mcp_server/redaction.py) and the tests.
## Structured errors
Every failure comes back as an `ErrorEnvelope`:
```json
{
"error": {
"code": "forbidden",
"message": "identity 'svc-analyst-01' is not permitted to call 'dispute_resolution.open'",
"request_id": "0d3f4a...",
"tool": "dispute_resolution.open",
"retryable": false,
"details": {}
}
}
```
Codes: `unauthenticated`, `forbidden`, `not_found`, `invalid_argument`,
`upstream_unavailable`, `rate_limited`, `internal`. Clients can branch on
`code` without string-matching messages.
## Deploy
Terraform under `terraform/` is a sketch of an internal ALB, ECS Fargate
service, and RDS Postgres for the audit log. It has not been applied
against a live account; treat it as a reference layout.
Prod checklist for a real rollout:
1. Populate a real `configs/rbac.yaml`. Prefer many narrow identities over
a few broad ones.
2. Wire the `X-Bank-Identity` header to be set by a trusted upstream (mTLS
terminator, OIDC proxy). The server does not authenticate; it only
reads the resolved identity.
3. Ship the audit table to your SIEM nightly. The table is the source of
truth.
4. Rotate the break-glass credential regularly and alert on every use.
## Repo layout
```
src/
mcp_server/ server, registry, rbac, audit, redaction, tools/
mcp_client/ http client used for testing and examples
configs/ default.yaml, rbac.yaml, prometheus.yml
terraform/ aws vpc + ecs + rds reference layout
tests/ rbac, audit, redaction, tools, registry
examples/ request/response transcripts
docs/ architecture, rbac model
```
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues