Skip to main content
Glama
aystzh
by aystzh
README.md
# Business Data MCP

**Secure, Auditable Read-only Search** · [中文](README.zh-CN.md)

A Python MCP server for **Codex and Claude Code**. Search PostgreSQL customers and orders, a JSON mock CRM, and Markdown business documents through authenticated, tenant-scoped tools with masking, source references, and audit logs.

All bundled business data is fictional. This is a reproducible local demonstration, not a production security certification. See [security boundaries](docs/security.md).

## Architecture

```mermaid
flowchart TD
    A[Codex / Claude Code / SDK client] -->|HTTP + Bearer token| B[Authentication]
    B --> C[Validation / scopes / rate limits / audit]
    C --> D[Business search service]
    D --> P[Read-only PostgreSQL]
    D --> R[JSON mock CRM]
    D --> F[Controlled Markdown files]
    P --> E[Masking / references / size limits]
    R --> E
    F --> E
    E --> C
    C --> L[JSON Lines audit]
    C --> A
```

Python 3.12, uv, MCP Python SDK 2.2, Pydantic, Psycopg async pools, PostgreSQL 17. The service exposes four typed read-only tools and no arbitrary SQL, arbitrary file reads, writes, pagination, or export.

## Quick start

Prerequisites: Docker with Compose and [uv](https://docs.astral.sh/uv/getting-started/installation/). Run commands from the repository root. uv selects or downloads Python 3.12.

```bash
uv sync --locked
uv run business-data-init
docker compose up -d --wait
uv run business-data-mcp
```

Initialization creates private (`0600`) `.env` and `.local` credential files without printing secrets or overwriting existing credentials. Development tokens expire after 30 days.

MCP: `http://127.0.0.1:8000/mcp`. PostgreSQL: loopback port `5433`, with a dedicated Compose volume.

In a second terminal at the repository root:

```bash
curl http://127.0.0.1:8000/health/ready
uv run business-data-demo
```

The SDK demo exercises four identities and all four tools without a paid model API. Use `--identity support`, `pii`, `tenant-b`, or `restricted` to select one. If `BUSINESS_DATA_MCP_TOKEN` is set, only that token is used.

## Tools and results

| Tool | Parameters | Behavior |
|---|---|---|
| `search_customers` | `query`, `limit=10` | Literal customer name substring or exact ID |
| `get_customer` | `customer_id` | Customer and authorized CRM summaries |
| `search_orders` | `order_no`, `phone`, `status`, `date_from`, `date_to`, `limit=10` | At least one filter, combined with AND |
| `search_business_docs` | `query`, `limit=10` | Tenant-scoped keywords, excerpts and line numbers |

Queries must contain 2–100 characters after trimming. IDs allow letters, digits, underscores and hyphens, up to 64 characters. Limits are integers from 1 to 20. Phone matching is exact. Order statuses: `pending`, `paid`, `shipped`, `cancelled`. Dates use inclusive UTC calendar days (`YYYY-MM-DD`). Document terms are whitespace-separated, all must match; results sort by occurrence count then document ID.

Results contain `records`, `sources`, `query_id`, `notices`, and `truncated`, in both `structuredContent` and text. Each record references source URIs such as `business://tenant-a/customers/C10001` or `crm://tenant-a/customers/C10001`. These are logical identifiers, not browsable URLs. Source timestamps come from the fixture metadata.

No match and inaccessible IDs both return empty records. Failures use MCP `isError`, with a stable error code and query ID; they are never disguised as empty results.

## Identities and audit

| Identity | Tenant | Permissions |
|---|---|---|
| `support` | tenant-a | All tools and CRM, masked PII |
| `pii` | tenant-a | Same, plus original PII |
| `tenant-b` | tenant-b | Only tenant B data, masked PII |
| `restricted` | tenant-a | Customers only; CRM omitted, orders/docs denied |

The server loads SHA-256 token digests and principals from `.local/tokens.json` at startup. Raw local demo tokens are in `.local/client-tokens.json`. Restart the server after changing the token store.

Audit events in `.local/audit.jsonl` contain start/finish, identity, tenant, tool, filter field names, result count, elapsed time, and status. They exclude raw tokens, query values, and business payloads. Authentication rejections have a separate request ID. Audit failure blocks data delivery.

```bash
rg 'qry_RETURNED_ID' .local/audit.jsonl
```

## Connect Codex / Claude Code: CLI and desktop

These instructions target **macOS clients and the MCP server on the same machine**. Run terminal commands from the repository root after completing Quick start. Model-account login and MCP token authentication are separate.

### Shared preparation: server and identity

```bash
curl -fsS http://127.0.0.1:8000/health/ready
```

Expect `{"status":"ready"}`. If unavailable, start the database and keep the server running in one terminal:

```bash
docker compose up -d --wait
uv run business-data-mcp
```

Open another terminal at the repository root. CLI examples use an environment variable; desktop examples use a local static header to avoid terminal environment inheritance:

```bash
export BUSINESS_DATA_MCP_TOKEN="$(uv run python -c 'import json; print(json.load(open(".local/client-tokens.json"))["support"])')"
```

This does not print the token. Repeat in new terminals and launch the CLI from the same terminal. To copy a token for desktop configuration on macOS:

```bash
uv run python -c 'import json; print(json.load(open(".local/client-tokens.json"))["support"], end="")' | pbcopy
```

Replace `PASTE_SUPPORT_TOKEN_HERE` below with the clipboard contents, keeping one space after `Bearer`. Store actual credentials only in private local configuration, never in README files, examples, or Git.

### 1. Codex CLI

Check `codex --version`. If it returns `zsh: command not found: codex` and your macOS app includes this executable:

```bash
# Check the file first; installation locations can differ.
test -x /Applications/ChatGPT.app/Contents/Resources/codex
export PATH="/Applications/ChatGPT.app/Contents/Resources:$PATH"
codex --version
```

Add the PATH export to `~/.zshrc` if you want it in future terminals. If this executable is absent, install the CLI using the [official Codex CLI instructions](https://developers.openai.com/codex/cli).

After loading the token environment variable above, register the server:

```bash
codex mcp add business-data \
  --url http://127.0.0.1:8000/mcp \
  --bearer-token-env-var BUSINESS_DATA_MCP_TOKEN
codex mcp get business-data
codex
```

Register once. `get` should show `enabled: true`, `transport: streamable_http`, the URL and token variable name; this confirms saved configuration only. Inside Codex, use `/mcp` to check the connection, then send the verification prompt below.

### 2. Codex desktop

Desktop and CLI share `~/.codex/config.toml` on the same Codex host, but an app launched from the Dock does not automatically inherit a terminal's `export`. For this local demo, use a static header:

1. Copy the token with the `pbcopy` command above.
2. Open `~/.codex/config.toml` using `open -e ~/.codex/config.toml`. If absent, first add a Streamable HTTP server named `business-data` through **Settings → MCP servers → Add server**.
3. Replace only the existing server section, preserving other configuration and avoiding duplicate sections:

```toml
[mcp_servers.business-data]
url = "http://127.0.0.1:8000/mcp"
enabled = true
http_headers = { Authorization = "Bearer PASTE_SUPPORT_TOKEN_HERE" }
```

Remove this server's `bearer_token_env_var` and any conflicting Authorization configuration in `env_http_headers` or a header helper. The CLI will then use the same static token without an export.

4. Save, enable the server under **Settings → MCP servers**, and select **Restart**. If unavailable, fully quit and reopen the app.
5. Start a new **local** conversation, inspect `/mcp`, and send the verification prompt.

See the [official Codex MCP documentation](https://developers.openai.com/codex/mcp). This server uses a development Bearer token, so OAuth sign-in is not needed.

### 3. Claude Code CLI

Check `claude --version` and sign in to the client. With the token exported in the same terminal:

```bash
claude --mcp-config examples/claude.mcp.json --strict-mcp-config
```

The example expands the environment variable:

```json
{
  "mcpServers": {
    "business-data": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "Authorization": "Bearer ${BUSINESS_DATA_MCP_TOKEN}"
      }
    }
  }
}
```

`--strict-mcp-config` limits this demo session to the specified MCP configuration. Use `/mcp` to inspect status, follow normal client tool approvals, then send the verification prompt. See the [official Claude Code MCP documentation](https://code.claude.com/docs/en/mcp).

### 4. Claude Code desktop (the Claude app's Code tab)

Select **Code → a local session** and open this repository. The Code tab can read the project's `.mcp.json`. To work when launched from the Dock, use a static header in `.mcp.json` at the repository root:

```json
{
  "mcpServers": {
    "business-data": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "Authorization": "Bearer PASTE_SUPPORT_TOKEN_HERE"
      }
    }
  }
}
```

If the file exists, merge only `mcpServers.business-data`; preserve other servers. This repository gitignores `.mcp.json`. Run `chmod 600 .mcp.json` to restrict local access. Do not copy actual credentials back into `examples/claude.mcp.json`.

Save and reopen a local Code session, trust the project and enable the MCP server when prompted, then send the verification prompt. Select the original checkout: a new worktree does not automatically include this gitignored configuration. CLI `--mcp-config` flags do not carry over to the desktop app. See the [official Claude Code Desktop documentation](https://code.claude.com/docs/en/desktop).

### Optional: Claude Desktop's ordinary Chat tab

Chat and Code have different setup entry points. Do not put `http://127.0.0.1:8000/mcp` into the account-level **Add custom connector** form: those requests originate from Anthropic's cloud and cannot reach your loopback address. See [remote connector network requirements](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).

For ordinary Chat, a local stdio-to-HTTP bridge can connect to this server. It requires Node.js / npx and third-party [mcp-remote](https://github.com/punkpeye/mcp-remote), which are not Python runtime dependencies of this project:

1. Check `node --version` and `npx --version`.
2. Open **Settings → Developer → Edit Config** in Claude. The usual macOS file is `~/Library/Application Support/Claude/claude_desktop_config.json`.
3. Merge this server into `mcpServers` and replace the token:

```json
{
  "mcpServers": {
    "business-data": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://127.0.0.1:8000/mcp",
        "--transport", "http-only", "--header", "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer PASTE_SUPPORT_TOKEN_HERE"
      }
    }
  }
}
```

4. Fully quit and reopen Claude, enable the tool in a new Chat conversation, and send the verification prompt. First use downloads the bridge. The desktop process must be able to find Node/npx. With nvm, set `command` to the absolute path from `command -v npx` and set `env.PATH` to include the directory containing `command -v node`, plus system paths. Recheck paths after Node upgrades.

These are configuration instructions; Claude desktop Code / Chat and the bridge have not been empirically verified here. See [verification results](docs/verification.md) for clients actually tested.

### Verify, switch identities, and troubleshoot

Send in any client:

> Actually call business-data's get_customer tool for C10001. Show customer details, CRM follow-ups, source URIs, and query_id. Do not read local files or use terminal commands to obtain the data.

With `support`, expect 星河科技, phone `138****8000`, the follow-up “已发送产品方案,等待技术评估”, source URIs `business://tenant-a/customers/C10001` and `crm://tenant-a/customers/C10001`, and a new `qry_...` ID. Match that ID to `query_started` / `query_finished` in `.local/audit.jsonl` to confirm a real call.

- **Switch identity:** for CLI environment-based auth, change `support` in the export command to `pii`, `tenant-b`, or `restricted`, then restart the client. For static auth, replace the configured token and reload. Do not reinitialize server credentials.
- **Authorization checks:** `restricted` querying `ORD-10001` returns `FORBIDDEN`; `pii` querying `C10001` shows the full phone; `tenant-b` querying the same order returns `cancelled`, `9900.00`.
- **401 / missing variable:** check the token, expiry and configured auth source. Avoid relying on terminal-only variables in desktop apps.
- **Connection refused:** check health and the server process; match the URL to `PORT`.
- **Tools missing:** reload MCP or start a new session; check enabled state, project directory, and duplicate server configuration overriding your entry.
- **Remote sessions:** in cloud or SSH sessions, `127.0.0.1` refers to the execution host, not your computer. These instructions target local sessions.

## Automated checks

```bash
make check                 # Ruff, formatting, mypy
make test                  # Unit tests; database tests explicitly skipped
make test-integration      # Full suite against the running demo database
```

GitHub Actions uses a PostgreSQL service, the same initialization script, and locked dependencies.

## Configuration and operations

See `.env.example`. Environment variables override `.env`. Paths resolve from the working directory.

| Setting | Default / meaning |
|---|---|
| `DATABASE_URL` | Required read-only connection string |
| `TOKEN_FILE` / `AUDIT_FILE` | `.local/tokens.json` / `.local/audit.jsonl` |
| `DATA_DIR` | `data`, a trusted local directory |
| `HOST` / `PORT` | `127.0.0.1` / `8000` |
| `RATE_LIMIT` | 60 tool calls per identity per minute |
| `QUERY_TIMEOUT` | 10 seconds; database statements separately limited to 5 seconds |
| `MAX_RESULT_BYTES` | Up to 65536 bytes per MCP tool result, including text and structured output |

Oversized results lose whole records and set `truncated`; a single oversized record returns `RESULT_TOO_LARGE`. Request bodies are limited to 16 KiB. `/health/live` checks process liveness; `/health/ready` checks the database.

Stop the server with Ctrl+C and the database with `docker compose stop`. Normal restarts preserve data.

**Reset deletes this project's demo database volume.** Stop the MCP server first:

```bash
bash scripts/reset-demo.sh --confirm-delete-demo-data
```

Reset keeps credentials. To regenerate credentials, first stop services and manually back up/move `.env`, `.local/tokens.json`, and `.local/client-tokens.json`; initialize again and reset the database. Changing passwords does not update an existing PostgreSQL volume.

### Troubleshooting

- macOS `docker-credential-desktop` missing: run `export PATH="/Applications/Docker.app/Contents/Resources/bin:$PATH"`, then retry Compose.
- Port 5433 occupied: use `POSTGRES_PORT=55433 uv run business-data-init` on first setup. For existing setup, update both the database port and `DATABASE_URL`.
- Port 8000 occupied: run `PORT=8001 uv run business-data-mcp` and change the client URL.
- Database unavailable: inspect `docker compose ps` and verify credentials match the initialized volume.
- HTTP 401: check token environment inheritance and expiry; restart server/client after token changes.
- `FORBIDDEN`: check scopes. The restricted identity's order denial is intentional.
- Remote client cannot connect: loopback only works on the same machine; this release does not configure remote HTTPS hosting.

## Roadmap

Real OAuth → PostgreSQL RLS → real CRM → centralized audit and distributed rate limiting → application containerization and deployment.

License: [Apache-2.0](LICENSE).