Skip to main content
Glama

triage-mcp

A read-only Postgres MCP server and a Claude-powered triage agent, from the tutorial "Build a Read-Only Postgres MCP Server for Automated Bug Triage".

  • sql/01_schema.sql: fictional SaaS billing schema, seed data, and a planted bug

  • sql/02_readonly_role.sql: the least-privilege triage_agent role

  • src/triage_mcp/sql_guard.py: parser-based SELECT validation (sqlglot)

  • src/triage_mcp/db.py: read-only, time-boxed query execution (psycopg)

  • src/triage_mcp/masking.py: PII scrubbing for every returned value

  • src/triage_mcp/server.py: the MCP server (streamable HTTP, bearer auth, audit log)

  • src/triage_mcp/agent.py: the triage agent (Claude tool runner + MCP client)

  • examples/ticket.json: a Microsoft List item as returned by Microsoft Graph

Requirements

  • Python 3.12 and uv 0.12

  • No Postgres install needed for local development: pgserver ships Postgres 16 binaries

  • Node.js 22.19+ only if you want to use MCP Inspector

  • An Anthropic API key only to run the agent against the real API (tests don't need one)

Pinned versions: mcp 2.2.0, anthropic 1.6.0, psycopg 3.3.5, sqlglot 30.18.0, uvicorn 0.53.0, pgserver 0.1.4 (PostgreSQL 16.2), pytest 9.1.1.

Related MCP server: Argos-MCP

Run the Tests

uv sync
uv run pytest

The tests start a throwaway Postgres, load the schema and role, run the MCP server under uvicorn, and call it over HTTP with the MCP Python client. The agent test replaces the Claude API with a scripted fake, so no API key is needed.

Run Locally

# 1. Start a local Postgres 16 with the demo data (prints connection strings)
uv run python scripts/local_db.py

# 2. Start the MCP server as the read-only role
export TRIAGE_DB_URL='postgresql://triage_agent:change-me@/billing?host=<socket dir printed above>'
export TRIAGE_MCP_TOKEN='local-dev-token'
uv run uvicorn triage_mcp.server:app --host 127.0.0.1 --port 8000

# 3. In another terminal, call a tool with MCP Inspector
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --transport http \
  --header "Authorization: Bearer local-dev-token" --method tools/list

# 4. Triage the example ticket (needs ANTHROPIC_API_KEY)
export TRIAGE_MCP_URL='http://127.0.0.1:8000/mcp'
uv run triage-agent examples/ticket.json

# Stop the local database when you're done
uv run python scripts/local_db.py --stop

Configuration

Variable

Used by

Purpose

TRIAGE_DB_URL

server

Connection string for the triage_agent role on the read replica

TRIAGE_MCP_TOKEN

server, agent

Shared bearer token (replace with IdP-issued JWTs in production)

TRIAGE_MCP_PUBLIC_URL

server

The URL clients use to reach /mcp

TRIAGE_MCP_ALLOWED_HOSTS

server

Comma-separated Host header allowlist, e.g. 10.0.1.25:*

TRIAGE_STATEMENT_TIMEOUT_MS

server

Per-query timeout (default 5000)

TRIAGE_MCP_URL

agent

Where the agent reaches the MCP server

ANTHROPIC_API_KEY

agent

Claude API credentials

The seed data, customer names, and schema are fictional.

Deployment Checklist (AWS)

  • Run sql/01_schema.sql (your real schema, views, and functions) and sql/02_readonly_role.sql on the primary; roles and grants replicate to RDS read replicas. Keep the role password in AWS Secrets Manager.

  • Point TRIAGE_DB_URL at the read replica endpoint with sslmode=verify-full and the RDS CA bundle.

  • Run the server on EC2 in a private subnet: uvicorn triage_mcp.server:app --host 0.0.0.0 --port 8000. Allow port 8000 only from the agent's security group, and 5432 on the replica only from the server.

  • Terminate TLS at an internal load balancer and run uvicorn with --proxy-headers --forwarded-allow-ips=<lb address>. Set TRIAGE_MCP_PUBLIC_URL and TRIAGE_MCP_ALLOWED_HOSTS to the internal hostname.

  • Replace StaticTokenVerifier with JWT validation against your identity provider (check audience and expiry).

  • Ship stderr (JSON audit lines prefixed with audit) to CloudWatch Logs and alert on rejected and timeout entries.

  • Monitor the replica's ReplicaLag metric; results can trail the primary.

  • Under load, replace per-call connections with psycopg_pool, sized below the role's CONNECTION LIMIT 10.

Troubleshooting

  • No module named 'mcp.server.fastmcp': mcp 2.x renamed FastMCP to MCPServer (from mcp.server import MCPServer).

  • 421 Misdirected Request: the request's Host header is not in TRIAGE_MCP_ALLOWED_HOSTS.

  • 401 Unauthorized: the Authorization: Bearer <token> header is missing or doesn't match TRIAGE_MCP_TOKEN.

  • Timestamps in a non-UTC offset: make sure the role has timezone = 'UTC' (set in 02_readonly_role.sql).

  • Audit lines wrapped across several lines: mcp[cli] pulls in rich, whose log handler wraps long messages. The audit logger uses its own StreamHandler with propagate = False to keep one JSON line per query.

  • Query timed out from run_select: add filters (user_id, a created_at range) or aggregate; the default timeout is 5 seconds (TRIAGE_STATEMENT_TIMEOUT_MS).

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    C
    maintenance
    Enables read-only exploration of a Postgres database using natural language, with multiple safety layers to prevent any modifications.
    5
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables Claude Code to securely interact with PostgreSQL, MySQL, SQLite, and SQL Server databases, featuring read-only mode, query validation, SSH tunneling, and field redaction for production-safe data access.
    2
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables an AI assistant to run guarded, read-only SQL queries against a Postgres database with enforced limits and validation.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to query a Postgres data warehouse through a governed, read-only SQL interface with policy enforcement, row limits, schema-level PII isolation, and a full audit trail.
    1
    -