Skip to main content
Glama
Sa3fa

pg-analytics-mcp

by Sa3fa

pg-analytics-mcp

Claude용 설정 기반, 읽기 전용 Postgres MCP 서버입니다. Postgres 스키마를 Streamable HTTP를 통해 Claude에 노출하며, 스키마와 enum 값은 부팅 시 라이브 데이터베이스에서 인트로스펙션하고, 클라이언트별 설정은 단일 YAML 파일에 담습니다.

Cloudflare Access 뒤에서 cloudflared → 리버스 프록시 스택으로 실행되도록 설계되었습니다(전체 프로비저닝 플레이북 포함). 그러나 서버 자체에는 Cloudflare 의존성이 없으며 어디서든 실행됩니다.

클라이언트 비종속적. server/ 아래의 어떤 것도 특정 클라이언트를 알지 못합니다. 새 클라이언트를 서비스하려면: 저장소를 복사하고, 설정 파일을 작성하고, .env를 설정하세요.

왜 이게 필요한가

이전 버전은 벤더 패키지를 우회하기 위해 세 개의 프로세스를 쌓았습니다:

supergateway  →  enrich.py  →  postgres-mcp  →  Postgres

postgres-mcp는 stdio/SSE만 지원하며(Cloudflare는 Streamable HTTP를 요구함), 설정 표면이 전혀 없고, supergateway는 MCP 세션마다 자식 프로세스를 포크했지만 회수되지 않았습니다 — 역할 제한 20개에 대해 23개 자식 프로세스 / 15개 연결이 측정되었고, 이는 "약 9회 호출 후 모든 것이 실패, SELECT 1 포함"으로 드러났습니다.

이 서버는 단일 프로세스에 단일 공유 풀을 사용합니다. 측정 결과: 30회 도구 호출 후 1개 프로세스.

Related MCP server: mcp-postgres

아키텍처

Claude → portal.<zone>          Cloudflare MCP Server Portal (OAuth)
       → mcp-origin.<zone>      Access app + Managed OAuth
       → cloudflared            tunnel
       → traefik                Host-header routing
       → this container         uvicorn, Streamable HTTP at /mcp
       → Postgres               read-only role → analytics.* views

보안 경계는 이 서버가 아니라 데이터베이스 역할입니다.

빠른 시작

cp .env.example .env      # set DATABASE_URI + the deployment vars
$EDITOR config/example.yaml   # domain prose for this client
docker compose up -d --build

curl -s localhost:8000/healthz        # ok
curl -s localhost:8000/introspection  # what the server decided at boot

그런 다음 Cloudflare 쪽은 docs/PLAYBOOK-NEW-CLIENT.md를 따르세요.

설정

.env — 호스트별 설정, VPS 간에 달라지는 유일한 것:

변수

용도

DATABASE_URI

읽기 전용 역할. Supavisor 풀러에서는 사용자 이름에 반드시 .PROJECT_REF가 포함되어야 합니다.

MCP_CONTAINER_NAME

컨테이너, 이미지 태그, traefik 라우터 이름

MCP_HOSTNAME

공개 호스트 이름; 전송 보안 허용 목록에 자동 추가됨

TRAEFIK_NETWORK

traefik이 감시하는 외부 도커 네트워크

MCP_CONFIG

이미지 내부의 클라이언트 YAML 경로

MCP_LOCAL_PORT

호스트 측 게시 포트(기본값 8000)

config/<client>.yaml — 도메인 영역. 여기에 열이나 enum 값을 나열하지 마세요: 부팅 시 라이브 데이터베이스에서 인트로스펙션되므로 오래될 수 없습니다. 인트로스펙션이 알 수 없는 것, 즉 비즈니스 의미와 함정만 작성하세요.

도구

기본 제공:

  • execute_sql(sql) — 원시 읽기 전용 SQL. 설명은 부팅 시 작성한 산문 더하기 생성된 스키마와 enum 목록으로 조합됩니다.

  • list_views() — 열, 행 수, enum이 포함된 모든 읽기 가능한 객체.

  • describe_view(name) — 한 객체의 열.

설정 정의: tools.queries 아래의 모든 항목은 타입이 지정된 매개변수를 가진 실제 MCP 도구가 됩니다. 매개변수는 psycopg 명명된 자리표시자를 통해 바인딩됩니다 — 문자열 보간이 절대 아님 — 그리고 min/max는 바인딩 전에 강제됩니다.

tools:
  queries:
    monthly_trend:
      description: |
        Donations per month. The most recent month is PARTIAL.
      params:
        months: {type: integer, default: 6, min: 1, max: 36}
      sql: |
        select ... where donated_at >= date_trunc('month', now())
                                     - make_interval(months => %(months)s - 1)

이것이 n8n과의 격차를 메우는 부분입니다: 도구 추가는 산문 + SQL이지, Python이 아닙니다.

설명이 여기 있는 이유

도구 설명은 모델이 도구를 사용할 수 있을 때마다 보는 유일한 컨텍스트입니다 — 모든 클라이언트, 모든 대화, 스킬 로딩이나 프로젝트 지시 없이. 외부 문서에 보관된 도메인 지식은 모델이 종종 갖지 못하는 지식입니다.

각 설명의 절반은 작성된 것이고(판단), 절반은 생성된 것입니다(사실). 생성된 절반 덕분에 boxy 플랫폼/프로세서와 daily 빈도가 이전의 수기 프롬프트에서처럼 누락될 수 없습니다.

운영

curl -s localhost:8000/introspection | python3 -m json.tool   # objects, enums, tools, limits
docker top <container>                                        # must stay at 1 process
docker compose up -d --build                                  # after a config edit

설정이나 스키마 변경은 재시작이 필요합니다 — 인트로스펙션은 의도적으로 프로세스 수명 동안 캐시되므로, 실행 중에 동작이 달라질 수 없습니다.

다섯 가지 경계 테스트

뷰, 권한, 설정을 변경한 후 다시 실행하세요. 다섯 가지 모두 실패해야 합니다:

update donations set amount = 0 where false;   -- permission denied for view
select count(*) from public.donations;         -- permission denied for table
select count(*) from public.website_orders;    -- permission denied for table
create table analytics.t (id int);             -- read-only transaction
select phone_number from customers limit 1;    -- column does not exist

limits.select_only는 존재하지만 기본값은 꺼짐: 역할이 경계이며, 그 위의 SQL 검증기는 이득 없이 유효한 읽기 전용 구문을 차단합니다 — 이것이 postgres-mcp의 제한 모드가 폐기된 이유입니다.

피로 대가로 얻은 함정들

  • Compose 레이블 키는 변수 치환되지 않습니다. 레이블은 목록 형식(- "traefik...=value")이어야 합니다. 그렇지 않으면 말 그대로 ${MCP_CONTAINER_NAME}이라는 이름의 라우터가 생기고 traefik이 404를 반환합니다.

  • DNS-리바인딩 보호는 MCP SDK에서 기본적으로 켜져 있습니다. 프록시 뒤에서 전달되는 Host가 허용되어야 합니다. MCP_HOSTNAME과 MCP_LOCAL_PORT는 자동으로 추가됩니다.

  • MCP 앱을 자체 Starlette 아래에 마운트하면 수명 주기가 대체됩니다. 세션 관리자를 명시적으로 시작해야 합니다(server.session_manager.run()). 그렇지 않으면 모든 요청이 "Task group is not initialized"로 500을 반환합니다.

  • set_read_only / set_autocommit은 연결에서 execute()보다 먼저 호출되어야 합니다. 그렇지 않으면 풀이 "connection in transaction status INTRANS"로 실패합니다.

  • pg_class.reltuples는 뷰에 대해 의미가 없습니다. 따라서 행 추정치는 부팅 시 제한된 count(*)로 대체됩니다.

  • Supavisor는 application_name을 "Supavisor"로 다시 씁니다. 따라서 풀러를 통한 클라이언트별 연결 귀속은 불가능합니다.

  • MCP SDK 2.0은 FastMCP를 MCPServer로 이름을 바꾸고 mcp.server.fastmcp 밖으로 이동시켰습니다. requirements.txt가 그 이유로 전체 잠금 파일입니다.

라이선스

MIT — LICENSE 참조.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Connects to a PostgreSQL database and exposes schema inspection and safe SELECT query execution as MCP tools and resources for use with Claude Desktop or any MCP-compatible client.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables read-only, SELECT-only querying of any Postgres database through MCP-compatible clients like Claude, with schema introspection and guarded SQL execution.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables Claude Desktop and Claude Code to inspect a Postgres schema and run read-only SQL queries against a demo database through the tools describe_schema and query_readonly. Every proposed query passes the same deterministic guardrails — SELECT-only, allow-listed tables, enforced row limits, statement timeouts and a read-only role — before execution.
    2
    MIT