Skip to main content
Glama
hc-hyun

postgres-mcp

by hc-hyun
README.md
# postgres-mcp

여러 PostgreSQL 데이터소스를 하나의 Streamable HTTP MCP 서버로 제공하는
작은 Text-to-SQL 백엔드입니다.

```text
AI / MCP client
        |
        | Streamable HTTP: http://host:8000/mcp
        v
postgres-mcp
  ├─ product_quality -> PostgreSQL connection pool
  └─ analytics       -> PostgreSQL connection pool
```

MCP 호스트의 AI가 자연어를 SQL로 변환합니다. 이 서버는 실제 데이터베이스
구조를 보여 주고, 선택한 데이터소스 한 곳에서 SQL 한 문장을 제한된
트랜잭션으로 실행합니다. 서로 다른 데이터소스를 한 SQL에서 조인하지
않습니다.

## MCP 계약

외부 도구는 두 개뿐입니다.

| 도구 | 역할 |
| --- | --- |
| `search_objects` | 데이터소스, 스키마, relation, 컬럼, 인덱스, 함수, 프로시저, 외래키 탐색 |
| `execute_sql` | 선택한 데이터소스에서 파라미터화 PostgreSQL 문장 하나 실행 |

권장 순서:

1. `search_objects(object_type="data_source")`로 데이터소스를 확인합니다.
2. 기본 스키마가 없으면 `object_type="schema"`로 스키마를 확인합니다.
3. 관련 테이블이나 뷰의 이름을 찾습니다.
4. 필요한 relation만 `detail_level="full"`로 조회합니다.
5. 확인된 식별자로 스키마 한정 SQL을 만들고 `execute_sql`로 실행합니다.

서버가 별도 MCP prompt를 제공하지는 않습니다. 서버 지침과 두 도구의
설명이 위 흐름을 안내합니다.

## 빠른 시작

Python 3.12 이상과 [uv](https://docs.astral.sh/uv/)가 필요합니다.

```bash
cp postgres-mcp.example.toml postgres-mcp.toml
cp .env.example .env
```

`postgres-mcp.toml`에 데이터소스를 정의합니다.

```toml
default_source = "product_quality"

[[sources]]
id = "product_quality"
description = "Product quality database"
host = "127.0.0.1"
port = 5433
database = "product_quality"
user_env = "DB_USER"
password_env = "DB_PASSWORD"
default_schema = "public"
```

호스트, 포트, 데이터베이스처럼 민감하지 않은 연결 정보는 TOML에 두고,
계정 값만 `user_env`, `password_env`가 가리키는 환경 변수에 넣습니다.

```dotenv
POSTGRES_MCP_CONFIG=postgres-mcp.toml
DB_USER=readonly
DB_PASSWORD=password
```

서버는 `.env`를 자동으로 읽지 않습니다. uv가 명시적으로 환경 변수를
주입하도록 실행합니다.

```bash
uv sync --locked
uv run --env-file .env postgres-mcp
```

기본 엔드포인트:

| URL | 용도 |
| --- | --- |
| `http://127.0.0.1:8000/mcp` | Streamable HTTP MCP |
| `http://127.0.0.1:8000/healthz` | 프로세스 liveness |
| `http://127.0.0.1:8000/readyz` | 설정 로드 및 요청 수신 readiness |

```bash
curl http://127.0.0.1:8000/healthz
curl http://127.0.0.1:8000/readyz
```

`readyz`는 데이터베이스 전체에 미리 연결하지 않습니다. 각 연결 풀은 해당
데이터소스를 처음 사용할 때 생성됩니다.

## 데이터소스 설정

`POSTGRES_MCP_CONFIG`가 가리키는 TOML 파일만 데이터베이스 설정으로
사용합니다.

| 항목 | 기본값 | 설명 |
| --- | --- | --- |
| `id` | 필수 | MCP 요청에서 사용할 안정적인 식별자 |
| `description` | `""` | AI가 데이터소스를 고를 때 참고할 설명 |
| `host` | 필수 | PostgreSQL 호스트 |
| `port` | `5432` | PostgreSQL 포트 |
| `database` | 필수 | 데이터베이스 이름 |
| `user_env` | 필수 | 사용자 이름이 들어 있는 환경 변수 이름 |
| `password_env` | 필수 | 비밀번호가 들어 있는 환경 변수 이름 |
| `ssl` | `"prefer"` | PostgreSQL SSL 모드 |
| `read_only` | `true` | 읽기 전용 트랜잭션 사용 |
| `max_rows` | `200` | 반환 가능한 최대 행 |
| `statement_timeout_ms` | `30000` | SQL 제한 시간 |
| `connection_timeout_ms` | `10000` | 연결 제한 시간 |
| `pool_size` | `4` | 데이터소스별 연결 풀 크기 |
| `default_schema` | 없음 | 객체 검색의 기본 스키마 |

선택 설정은 공통 블록으로 우회하지 않고 데이터소스마다 직접 적습니다.
여러 데이터소스가 같은 계정을 사용하면 같은 `user_env`와 `password_env`를
재사용합니다. 별도 권한이 필요한 데이터소스만 다른 환경 변수 이름을
지정합니다.

```toml
default_source = "product_quality"

[[sources]]
id = "product_quality"
description = "Product quality database"
host = "127.0.0.1"
port = 5433
database = "product_quality"
user_env = "DB_USER"
password_env = "DB_PASSWORD"
default_schema = "public"
pool_size = 2

[[sources]]
id = "analytics"
description = "Quality analytics database"
host = "127.0.0.1"
port = 5433
database = "analytics"
user_env = "DB_USER"
password_env = "DB_PASSWORD"
default_schema = "public"
pool_size = 2

[[sources]]
id = "audit"
description = "Restricted audit database"
host = "audit-db.internal"
database = "quality_audit"
user_env = "AUDIT_DB_USER"
password_env = "AUDIT_DB_PASSWORD"
default_schema = "audit"
pool_size = 1
```

`default_source`가 없고 소스가 여러 개면 도구 호출에 `data_source`를
명시해야 합니다. 소스가 하나면 자동으로 선택됩니다.

현재 구현은 PostgreSQL만 지원합니다. 다른 RDB가 실제 요구사항이 되기
전에는 공통 드라이버 계층이나 adapter 추상화를 두지 않습니다.

## Codex 연결

서버를 먼저 실행하고 프로젝트 `.codex/config.toml`에 URL을 등록합니다.

```toml
[mcp_servers.postgres-mcp]
url = "http://127.0.0.1:8000/mcp"
startup_timeout_sec = 10
tool_timeout_sec = 60
```

새 Codex 세션부터 MCP 연결이 초기화됩니다.

## HTTP 설정

MCP 경로는 `/mcp`, HTTP 모드는 stateless, 응답은 JSON으로 고정됩니다.
다음 운영 설정만 환경 변수로 조정할 수 있습니다.

| 환경 변수 | 로컬 기본값 | 설명 |
| --- | --- | --- |
| `POSTGRES_MCP_HOST` | `127.0.0.1` | 바인드 주소 |
| `POSTGRES_MCP_PORT` | `8000` | 바인드 포트 |
| `POSTGRES_MCP_LOG_LEVEL` | `INFO` | 서버 로그 수준 |
| `POSTGRES_MCP_ALLOWED_HOSTS` | 로컬 Host | 허용할 Host 헤더의 쉼표 목록 |
| `POSTGRES_MCP_ALLOWED_ORIGINS` | 로컬 Origin | 허용할 브라우저 Origin의 쉼표 목록 |

애플리케이션 로그는 터미널에서 Rich 색상과 읽기 쉬운 레벨·시간 형식으로
표시되고, 비대화형 출력에서는 ANSI 색상을 자동으로 생략합니다. 기본
`INFO`는 서버와 connection pool 수명주기만 기록합니다. `DEBUG`는 도구별
처리 시간과 결과 행 수를 추가하지만 SQL, parameter, 자격증명 값은 기록하지
않습니다.

`POSTGRES_MCP_HOST=0.0.0.0`처럼 외부 인터페이스에 바인드하면
`POSTGRES_MCP_ALLOWED_HOSTS`를 반드시 지정해야 합니다. 정확한 hostname과
`hostname:*` 패턴을 사용하고 광범위한 와일드카드는 피하십시오.

## Docker

```bash
docker build -t postgres-mcp:local .
```

호스트 머신의 PostgreSQL에 연결한다면 마운트할 TOML의 `host`를
`"host.docker.internal"`로 바꿉니다. `127.0.0.1`은 컨테이너 자신을
가리킵니다.

```bash
docker run --rm -p 8000:8000 \
  --add-host host.docker.internal:host-gateway \
  --mount type=bind,src="$(pwd)/postgres-mcp.toml",dst=/etc/postgres-mcp/postgres-mcp.toml,readonly \
  -e POSTGRES_MCP_CONFIG=/etc/postgres-mcp/postgres-mcp.toml \
  -e DB_USER=readonly \
  -e DB_PASSWORD=password \
  -e POSTGRES_MCP_ALLOWED_HOSTS="localhost,localhost:*,127.0.0.1,127.0.0.1:*" \
  postgres-mcp:local
```

설정 파일을 이미지에 포함하지 말고 읽기 전용으로 mount하십시오. 운영
환경에서는 TOML의 `host`에 실제 DB DNS를 사용하고, 계정은 명령줄 대신
런타임 Secret으로 주입하십시오.

## Kubernetes

[`deploy/k8s/postgres-mcp.yaml`](deploy/k8s/postgres-mcp.yaml)은 다음을
포함합니다.

- 데이터소스 하나를 정의한 ConfigMap
- `POSTGRES_MCP_CONFIG` 파일 mount
- Secret의 `DB_USER`, `DB_PASSWORD`
- readiness/liveness probe
- 비루트, read-only filesystem 보안 설정

```bash
kubectl create secret generic postgres-mcp-secrets \
  --from-literal=db-user='readonly' \
  --from-literal=db-password='password'
kubectl apply -f deploy/k8s/postgres-mcp.yaml
kubectl port-forward service/postgres-mcp 8000:8000
```

상세한 운영 절차는 [`docs/deployment.md`](docs/deployment.md)를
참고하십시오.

연결 위치와 정책은 ConfigMap, 계정 값은 Secret으로 분리합니다. 어느
쪽이든 바뀌면 명시적으로 rollout하여 새 설정과 연결 풀을 함께 적용합니다.
권한, 네트워크, 감사 기준이 같은 데이터베이스만 한 배포에 묶고 보안
경계가 다르면 postgres-mcp 배포도 분리하십시오.

## 안전 기본값

- 기본 읽기 전용 PostgreSQL 트랜잭션
- prepared statement를 통한 단일 문장 제한
- `$1`, `$2`, ... 파라미터 바인딩
- 소스별 행 수, statement timeout, connection timeout
- 큰 정수, 날짜, UUID, `bytea`의 JSON 안전 변환
- 자격증명을 MCP 입력이나 결과에 노출하지 않음
- Host/Origin 검사로 DNS rebinding 방어

`read_only=true`는 보조 방어입니다. 운영에서는 각 데이터소스에 필요한
`SELECT` 권한만 가진 PostgreSQL 역할을 사용하고, 외부 공개 시 TLS와 인증을
담당하는 gateway를 두십시오.

## 개발

```bash
uv sync
uv run pytest
uv run ruff check src tests
uv run ruff format --check src tests
uv run pyright
uv build
```

구현과 책임 경계는 다음 문서에 정리되어 있습니다.

- [`docs/architecture-v0.4.0.md`](docs/architecture-v0.4.0.md)
- [`docs/service-boundaries.md`](docs/service-boundaries.md)
- [`docs/code-story-ko.md`](docs/code-story-ko.md)