Skip to main content
Glama
aystzh
by aystzh

Business Data MCP

Secure, Auditable Read-only Search · 中文

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.

Architecture

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.

Related MCP server: PostgreSQL MCP Server

Quick start

Prerequisites: Docker with Compose and uv. Run commands from the repository root. uv selects or downloads Python 3.12.

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:

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.

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

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:

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:

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:

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:

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

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

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:

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

  1. Save, enable the server under Settings → MCP servers, and select Restart. If unavailable, fully quit and reopen the app.

  2. Start a new local conversation, inspect /mcp, and send the verification prompt.

See the official Codex MCP documentation. 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:

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

The example expands the environment variable:

{
  "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.

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:

{
  "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.

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.

For ordinary Chat, a local stdio-to-HTTP bridge can connect to this server. It requires Node.js / npx and third-party 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:

{
  "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"
      }
    }
  }
}
  1. 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 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

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

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables read-only access to PostgreSQL databases with multi-tenant support, allowing users to query data, explore schemas, inspect table structures, and view function definitions across different tenant schemas safely.
    55 npm
    1
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides read-only access to PostgreSQL databases with schema inspection, query execution in multiple formats (JSON, CSV, Markdown), and query history tracking with built-in security features.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides secure read-only SQL access to PostgreSQL and ClickHouse databases with built-in safety features like read-only enforcement, timeouts, and managed result files.
    221 PyPI
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables e-commerce clients to query their own analytics data in plain English with strict tenant isolation enforced by the database.
    MIT