# MCP Hub Database 연동 상세 가이드

> **대상**: 사내 DBA / DB 담당자 — 4개 DB 종류 별 사용자 권한 / 연결 정보 / 트러블슈팅
> **선행 조건**: 커넥터 설치 + 툴킷 사용 가이드 숙지 ([Toolkit 사용 가이드](/guides/toolkit-usage))
> **본 문서는 DB 측 작업** 중심 — Studio 폼 입력은 툴킷 가이드 §5 참고

---

## 공통 보안 원칙

1. **읽기 전용 사용자 별도 생성** — 기존 운영 계정 재사용 X
2. **꼭 필요한 것만 권한 부여** — 조회 계정은 `SELECT` 만, 그것도 **필요한 테이블 / view 만**. 함수·프로시저를 실행하는 권한(`EXECUTE`) 은 주지 마세요 (DB 종류별 설명은 아래 각 절 참고)
3. **노출 범위 제한** — 모든 스키마 X. 비즈니스 분석 필요한 view / 테이블만 권한
4. **비밀번호는 secrets 파일** 에만 — Studio / Hub 에 절대 평문 X
5. **네트워크**: 커넥터 호스트에서만 접근 가능하도록 DB 측 ACL 조정 권장
6. **롤(role, 권한을 묶어둔 그룹) 밖의 전용 계정** — 권한은 롤을 거치지 말고 계정에 직접 주세요 (아래 "롤과 자동 권한 점검" 참고)

### 조회(읽기) 경로를 어떻게 봐야 하나 — 솔직한 설명

조회 도구는 AI 가 그때그때 필요한 `SELECT` 문을 **직접 만들어** 실행합니다. 그래서 조회 경로는 "설계상 위험이 0" 인 구조가 아닙니다 — **이 계정이 볼 수 있는 범위가 곧 문제가 생겼을 때의 영향 범위**입니다.

제품에는 여러 겹의 안전장치가 있습니다 (조회 문장만 허용 · 여러 문장 붙여 실행 차단 · 결과 행수 상한 · 응답 크기 상한 · 쿼리 시간 제한). 하지만 가장 확실한 방법은 **계정이 볼 수 있는 것 자체를 좁히는 것**입니다. 스키마 전체가 아니라 **업무에 필요한 테이블 / view 만** 골라서 권한을 주세요.

> 반대로 **입력(쓰기) 경로는 범위가 구조적으로 정해져 있습니다.** AI 는 관리자가 미리 검토해 확정한 **그 INSERT 문만**, 그것도 **뷰에 선언한 행 수 상한(기본 1행) 안에서만** 실행할 수 있습니다. 상한을 넘으면 확정 전에 되돌립니다. 여기에 더해 **쓰기 계정에는 그 테이블 INSERT 권한만 주시길 권장합니다** (아래 1-3 참고) — 이건 제품이 강제하는 것이 아니라 마지막 안전장치를 고객사에서 직접 세우는 부분입니다. 위 조회 쪽 주의사항이 쓰기 쪽까지 확대되는 것은 아닙니다.

### 가장 강력한 선택 — 읽기 전용 복제본 (read replica)

여건이 된다면 운영 DB 대신 **읽기 전용 복제본** (운영 DB 의 내용을 실시간으로 따라 복사하는 읽기 전용 사본) 에 연결하는 것이 가장 안전합니다.

- 서버 자체가 쓰기를 받지 않으므로 어떤 경로로도 데이터가 바뀌지 않습니다
- 무거운 조회가 돌아도 운영 DB 성능에 영향을 주지 않습니다

필수는 아닙니다 — 복제본이 없어도 아래의 최소권한 계정 방식으로 충분히 운영할 수 있습니다. 다만 복제본을 쓸 수 있다면 그쪽을 권장합니다.

### 아주 큰 조회 결과 — 제품 상한만 믿지 마세요

커넥터는 조회 결과를 **기본 8MB 까지만 전달**합니다(환경변수 `MCP_CONNECTOR_MAX_RESPONSE_BYTES` 로 조정). 다만 이건 **전달하는 양의 상한**이지, 데이터베이스에서 **받는 양의 상한이 아닙니다** — 데이터베이스 드라이버가 결과를 메모리에 먼저 올린 **뒤** 잘라내는 구조이기 때문입니다.

그래서 **조회 한 건의 결과 자체가 수백 MB** 인 경우(예: 아주 큰 파일·문서가 통째로 들어 있는 컬럼 한 칸) 이 상한은 커넥터를 지켜주지 못합니다. 이런 경우는 **DB 쪽에서 미리 막아 주시는 것이 확실합니다.**

- **조회 전용 복제본**에 연결 (위 참고)
- **큰 값이 들어 있는 컬럼은 view 로 좁혀서** 노출 — 원본 컬럼 대신 앞부분만 잘라낸 컬럼을 담은 view 를 만들고, 조회 계정에는 그 view 만 `SELECT` 권한을 주세요 (아래 각 DB 절의 "노출 범위 좁히기" 참고)
- **커넥터 호스트·컨테이너에 메모리 여유 확보** — 설치 안내대로 `--restart unless-stopped` 로 실행하셨다면, 혹시 커넥터가 메모리 부족으로 종료돼도 도커가 곧바로 다시 띄웁니다

### 롤 (role) 과 자동 권한 점검

Studio 에 DB 접속 정보를 저장하면 **그 계정의 실제 권한을 자동으로 점검**합니다.

이때 계정이 여러 롤을 거쳐 권한을 물려받는 구조 — 특히 **물려받지 않도록 설정된 롤** (PostgreSQL 의 `NOINHERIT` / `WITH INHERIT FALSE`) 이 중간에 끼어 있으면 — 실제 권한을 확정할 수 없습니다. 이 경우 점검은 추측하지 않고 **"확인 불가"** 로 정직하게 보고하며, **안전을 위해 쓰기 뷰는 막힙니다.**

따라서 조회·쓰기 계정 모두 **롤 구조 밖의 전용 계정**으로 만들고, 권한을 계정에 **직접** 부여하는 것을 권장합니다. 그러면 점검이 확정되고 안내도 정확해집니다.

> 자동 점검은 참고용입니다 — 100% 확정이 아니니 DB 계정의 실제 권한은 꼭 직접 확인해 주세요.

### Studio 폼 입력 — placeholder 사용 정책

| 필드                          | 평문 입력 | placeholder 사용                                      |
| ----------------------------- | --------- | ----------------------------------------------------- |
| **비밀번호**                  | ✗ (금지)  | ✅ **반드시** `${DB_PASSWORD}` 등                     |
| Host / Port / Database / User | ✅ 가능   | ✅ 가능 — `${DB_HOST}`, `${DB_NAME}`, `${DB_USER}` 등 |

- 본 가이드의 각 dialect 연결 정보 표는 **평문 예시** 로 적었지만, 어떤 필드든 `${VAR}` placeholder 로 치환 가능합니다. 그 경우 secrets 파일 (`/etc/mcp-connector/secrets/*.env`) 에 같은 이름의 변수도 함께 추가하세요.
- 사내 보안팀이 **토폴로지 (호스트/DB 이름/계정명) 평문 저장도 금지** 한다면 모두 placeholder 로, 그렇지 않으면 비밀번호만 placeholder — **두 방식 다 정상 동작**.
- 자세한 정책은 [Toolkit 사용 가이드](/guides/toolkit-usage) §3 참고.

> **⚠️ 비밀번호에 `#` `@` `:` `/` `?` 가 있으면 그 자리만 아래처럼 바꿔 적어주세요** (secrets 파일에 넣을 때).
> 접속 주소에서 구분자로 쓰이는 문자들이라 그대로 두면 접속이 실패하는데, 오류 메시지
> (`Login failed`, `invalid db url`)만으로는 비밀번호가 원인인 줄 알기 어렵습니다.
>
> | 문자 | 바꿔 적을 값 |     | 문자 | 바꿔 적을 값 |
> | ---- | ------------ | --- | ---- | ------------ |
> | `#`  | `%23`        |     | `/`  | `%2F`        |
> | `@`  | `%40`        |     | `?`  | `%3F`        |
> | `:`  | `%3A`        |     | `%`  | `%25`        |
>
> 예: 실제 비밀번호가 `Pa55#word@2024` 라면 → `DB_PASSWORD=Pa55%23word%402024`
> **데이터베이스의 비밀번호를 바꾸는 게 아니라, secrets 파일에 적는 표기만 바꾸는 것입니다.**
> (따옴표로 감싸는 것으로는 해결되지 않습니다.)

---

## 1. PostgreSQL

### 1-1. 읽기 전용 사용자 생성

**PostgreSQL 은 `SELECT` 만 주는 것으로 부족합니다.** PostgreSQL 은 데이터베이스 안 함수의 **실행 권한을 기본적으로 모든 사용자에게 열어둡니다.** 그래서 조회 권한만 가진 계정도 사내에서 만든 함수를 전부 호출할 수 있고, 그 함수가 "만든 사람의 권한으로 동작" 하도록 정의돼 있으면 조회 계정을 통해 데이터가 바뀔 수 있습니다. 아래 `REVOKE EXECUTE ... FROM PUBLIC` 두 줄이 이 기본값을 닫아 줍니다. (실제 DB 로 확인했습니다 — `count()` · `now()` 같은 일반적인 조회 쿼리는 이 설정 후에도 그대로 동작합니다.)

```sql
CREATE ROLE mcp_reader LOGIN PASSWORD '<강한 비밀번호>';
-- 롤 멤버십은 주지 않습니다 (권한 자동 점검이 "확인 불가"로 막힙니다)
GRANT CONNECT ON DATABASE erp TO mcp_reader;
GRANT USAGE ON SCHEMA public TO mcp_reader;
GRANT SELECT ON TABLE orders, customers TO mcp_reader;   -- 필요한 테이블만이 최선
REVOKE EXECUTE ON ALL FUNCTIONS  IN SCHEMA public FROM PUBLIC;
REVOKE EXECUTE ON ALL PROCEDURES IN SCHEMA public FROM PUBLIC;  -- PG11+
REVOKE CREATE ON SCHEMA public FROM PUBLIC;             -- PG14 이하만 (PG15+ 기본)
```

각 줄의 의미:

- `GRANT SELECT ON TABLE ...` — 업무에 필요한 테이블만 나열합니다. 이 목록이 곧 AI 가 볼 수 있는 범위입니다
- `REVOKE EXECUTE ...` (2줄) — 모든 사용자에게 열려 있던 **함수·프로시저 실행 기본값**을 닫습니다. 프로시저 쪽은 PostgreSQL 11 이상에서만 있습니다
- `REVOKE CREATE ON SCHEMA public ...` — 아무나 `public` 스키마에 새 객체를 만들지 못하게 합니다. **PostgreSQL 15 부터는 기본값**이라 14 이하에서만 필요합니다
- 롤 멤버십은 주지 않습니다 — 위 "롤과 자동 권한 점검" 참고

> ⚠️ **`REVOKE ... FROM PUBLIC` 은 이 계정만이 아니라 DB 전체에 적용됩니다.**
> `PUBLIC` 은 "모든 사용자" 를 뜻하는 특별한 대상이라, 위 `REVOKE EXECUTE` 두 줄을 실행하면 **기존 사내 애플리케이션도 그 함수들을 못 쓰게 됩니다.** 반드시 아래 순서로 진행하고, **가능하면 스테이징(테스트) 환경에서 먼저 시험해 보세요.**
>
> 1. `REVOKE EXECUTE ... FROM PUBLIC` 으로 기본값을 닫습니다
> 2. 그 함수가 정말로 필요한 롤에만 다시 열어 줍니다
>    ```sql
>    GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO app_role;
>    -- 또는 함수 하나씩: GRANT EXECUTE ON FUNCTION public.calc_total(int) TO app_role;
>    ```
> 3. 앞으로 새로 만들어질 함수도 같은 기본값이 되지 않도록 미리 정해 둡니다
>    ```sql
>    ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public
>      REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
>    ```
>    (`app_owner` = 앞으로 함수를 만들 계정. 함수를 만드는 계정마다 한 번씩 지정해야 합니다.)

### 1-2. 조회 계정 권한 — 주지 말아야 할 것 · 더 좁히기

**절대 주면 안 되는 권한**

| 주지 말아야 할 것               | 이유                                                                     |
| ------------------------------- | ------------------------------------------------------------------------ |
| `pg_read_all_data` 롤           | 테이블별로 준 조회 권한을 **통째로 무시하고 모든 데이터**를 읽게 합니다  |
| `pg_write_all_data` 롤          | 모든 테이블에 쓰기가 가능해집니다                                        |
| `SUPERUSER`                     | 모든 제한이 사라집니다                                                   |
| `CREATEROLE`                    | 스스로 더 강한 계정을 만들 수 있습니다                                   |
| 함수 실행 권한(`EXECUTE`)       | 1-1 참고 — 조회 계정으로 데이터가 바뀔 수 있는 경로가 열립니다           |
| 롤 멤버십 (`GRANT <롤> TO ...`) | 실제 권한을 자동 점검이 확정하지 못해 "확인 불가" 가 됩니다 (위 공통 절) |

**설정 후 확인** — 조회 계정으로 접속해서 아래를 실행하면 둘 다 `false` 여야 정상입니다.

```sql
SELECT has_function_privilege(current_user, 'some_func()', 'EXECUTE');  -- false 여야 정상
SELECT has_table_privilege(current_user, 'orders', 'UPDATE');           -- false 여야 정상
```

**노출 범위 더 좁히기 (권장)** — 비즈니스 view 만 노출하는 방식이 가장 안전합니다.

```sql
CREATE VIEW analytics.v_orders_summary AS
  SELECT id, customer_id, total, created_at FROM orders;

GRANT USAGE ON SCHEMA analytics TO mcp_reader;
GRANT SELECT ON analytics.v_orders_summary TO mcp_reader;
```

> 스키마 전체에 `GRANT SELECT ON ALL TABLES` 를 주고 싶더라도, 그만큼 영향 범위가 넓어집니다. 업무에 필요한 테이블 / view 를 나열하는 쪽을 권장합니다.

### 1-3. 쓰기 전용 사용자 (쓰기 뷰를 쓸 때만)

쓰기 뷰(관리자가 미리 승인한 INSERT 문) 를 쓰는 경우, 조회 계정과 **별도로** 쓰기 계정을 만들고 **대상 테이블 INSERT 권한만** 주세요.

> **이 계정 권한이 AI 가 할 수 있는 일의 최종 한계선입니다.** AI 는 관리자가 등록한 그 INSERT 문만, 정해둔 행 수 안에서만 실행하지만, 그 위의 어떤 안전장치보다 확실한 것은 계정에 아예 권한이 없는 것입니다. 수정·삭제 권한은 주지 마세요.

```sql
CREATE USER mcp_writer WITH PASSWORD 'Pa55word!2024';
GRANT CONNECT ON DATABASE erp TO mcp_writer;

\c erp
GRANT USAGE ON SCHEMA public TO mcp_writer;
GRANT INSERT ON public.order_notes TO mcp_writer;

-- id 컬럼이 serial 타입이면 시퀀스 사용 권한도 필요합니다 (아래 설명 참고)
GRANT USAGE ON SEQUENCE public.order_notes_id_seq TO mcp_writer;
```

> **표가 `public` 이 아닌 스키마에 있으면 그 스키마 이름으로 바꿔주세요.**
> 위 예시는 표가 `public` 스키마에 있는 경우입니다. 예를 들어 표가 `sales` 스키마에 있으면 `GRANT USAGE ON SCHEMA sales TO mcp_writer;` 와 `GRANT INSERT ON sales.order_notes TO mcp_writer;` 처럼 바꿔 주세요. **표에 INSERT 권한을 줘도 그 스키마에 `USAGE` 가 없으면 접근 자체가 막혀** `permission denied for schema sales` 로 실패합니다. 데이터베이스 이름과 스키마 이름이 같은 경우(예: `sales` DB 안의 `sales` 스키마) 특히 놓치기 쉽습니다.

> **`serial` 컬럼이 있으면 INSERT 권한만으로는 실패합니다.**
> `id serial PRIMARY KEY` 처럼 번호를 자동으로 매기는 컬럼은 뒤에 숨은 **시퀀스** 객체에서 다음 번호를 받아옵니다. 이 시퀀스는 테이블과 권한이 따로라, INSERT 권한만 주면 입력이 `permission denied for sequence order_notes_id_seq` 로 실패합니다. 위 예시처럼 `GRANT USAGE ON SEQUENCE <시퀀스이름> TO <쓰기계정>;` 을 함께 주세요.
> 시퀀스 이름은 보통 `<테이블>_<컬럼>_seq` 이고, 정확한 이름은 이렇게 확인합니다:
>
> ```sql
> SELECT pg_get_serial_sequence('public.order_notes', 'id');
> ```
>
> **PostgreSQL 10 이상의 `IDENTITY` 컬럼**(`id int GENERATED BY DEFAULT AS IDENTITY`)은 이 권한이 **필요 없습니다** — INSERT 권한만으로 동작합니다.

생성된 id 를 돌려받는(`RETURNING id`) 뷰를 쓰려면 그 테이블의 `SELECT` 권한도 함께 필요합니다.

### 1-4. 연결 정보 (Studio 폼 입력값)

| 필드     | 값                                                                                          |
| -------- | ------------------------------------------------------------------------------------------- |
| Host     | `pg-erp.internal`                                                                           |
| Port     | `5432`                                                                                      |
| Database | `erp`                                                                                       |
| User     | `mcp_reader`                                                                                |
| 비밀번호 | `${DB_PASSWORD}`                                                                            |
| SSL      | `사용 (CA 검증 없음)` — 사내 자체서명일 때<br>`사용 (시스템 CA)` — Let's Encrypt 등 공인 CA |

### 1-5. secrets 파일

`/etc/mcp-connector/secrets/pg.env`:

```
DB_PASSWORD=Pa55word!2024
```

### 1-6. 사전 연결 테스트 (커넥터 호스트에서)

```bash
psql "postgres://mcp_reader:Pa55word!2024@pg-erp.internal:5432/erp" -c "SELECT 1"
# 출력: ?column? \n ---------- \n 1
```

### 1-7. 트러블슈팅

| 증상                               | 원인 / 해결                                                                                              |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `password authentication failed`   | 사용자/비밀번호 재확인 — `\password` 로 재설정 가능                                                      |
| `connection refused`               | PG 의 `pg_hba.conf` 에 커넥터 호스트 IP 허용 라인 추가 필요                                              |
| `permission denied for table X`    | GRANT SELECT 누락 — 위 1-1 절 다시                                                                       |
| `SSL connection failed`            | `pg_hba.conf` 의 method 가 `hostssl` 이면 SSL 필수. Studio 의 SSL 옵션 `require` 이상으로                |
| `permission denied for sequence X` | 쓰기 계정에 `serial` 컬럼의 시퀀스 사용 권한 누락 — 위 1-3 절의 `GRANT USAGE ON SEQUENCE` 참고           |
| `permission denied for schema X`   | 표에 INSERT/SELECT 권한은 줬지만 그 표가 속한 스키마에 `USAGE` 누락 — `GRANT USAGE ON SCHEMA X TO 계정;` |

---

## 2. MySQL / MariaDB

### 2-1. 읽기 전용 사용자 생성

```sql
CREATE USER 'mcp_reader'@'%' IDENTIFIED BY 'Pa55word!2024';
GRANT SELECT ON appdb.* TO 'mcp_reader'@'%';
FLUSH PRIVILEGES;
```

> ⚠️ **조회 계정에 `EXECUTE` (함수·프로시저 실행 권한) 는 주지 마세요.**
> MySQL 은 기본적으로 조회 계정에 실행 권한을 주지 않습니다 — **이 기본값이 조회 계정의 가장 중요한 안전장치**이니 그대로 두세요. (실제 DB 로 확인했습니다: `SELECT` 만 가진 계정은 모든 함수 호출이 DB 에서 거부됐습니다.)
> MySQL 은 함수를 만들 때 별도로 지정하지 않으면 **"함수를 만든 사람의 권한으로 동작"** 이 기본값입니다. 리포팅 편의를 위해 `GRANT EXECUTE` 를 한 번 주면, 그 함수 안의 입력·수정 작업이 조회 계정을 통해 실행될 수 있습니다.

### 2-2. 노출 범위 좁히기

```sql
-- 특정 테이블만
GRANT SELECT ON appdb.orders, appdb.customers TO 'mcp_reader'@'%';

-- view 만 (권장)
CREATE VIEW appdb.v_orders_summary AS
  SELECT id, customer_id, total, created_at FROM orders;
GRANT SELECT ON appdb.v_orders_summary TO 'mcp_reader'@'%';
```

### 2-3. 연결 정보

| 필드     | 값                         |
| -------- | -------------------------- |
| Host     | `mysql-app.internal`       |
| Port     | `3306`                     |
| Database | `appdb`                    |
| User     | `mcp_reader`               |
| 비밀번호 | `${DB_PASSWORD}`           |
| SSL      | `사용 (CA 검증 없음)` 권장 |

### 2-4. secrets 파일

`/etc/mcp-connector/secrets/mysql.env`:

```
DB_PASSWORD=Pa55word!2024
```

### 2-5. 사전 연결 테스트

```bash
mysql -h mysql-app.internal -P 3306 -u mcp_reader -p'Pa55word!2024' appdb -e "SELECT 1"
```

### 2-6. 트러블슈팅

| 증상                                                  | 원인 / 해결                                                                                                                                                                                                                    |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Access denied for user 'mcp_reader'@'커넥터호스트'`  | `'mcp_reader'@'%'` 가 아닌 특정 호스트만 허용된 사용자. `%` 또는 커넥터 호스트 IP 명시                                                                                                                                         |
| `Unknown database 'appdb'`                            | DB 이름 오타 — 대소문자 구분 (`SHOW DATABASES;` 확인)                                                                                                                                                                          |
| MySQL 5.7 + `caching_sha2_password` 인증 오류         | 커넥터의 mysql2 드라이버 호환. 비밀번호를 `mysql_native_password` 로 재설정 또는 MySQL 8 권장                                                                                                                                  |
| TLS handshake error                                   | SSL 옵션 mismatch — MySQL 의 `require_secure_transport=ON` 이면 SSL `require` 필수                                                                                                                                             |
| 쓰기 뷰 실행이 `write_unsupported_non_innodb` 로 거절 | 대상 DB 에 MyISAM 등 InnoDB 가 아닌 테이블이 있습니다. 이런 엔진은 작업을 되돌리는 기능(롤백)이 없어 안전장치가 무효라 쓰기를 거절합니다. `ALTER TABLE <표> ENGINE=InnoDB` 로 전환 후 사용하세요 (조회는 엔진과 무관하게 동작) |

---

## 3. MS SQL Server

### 3-1. 읽기 전용 로그인 + 사용자 생성

```sql
USE master;
CREATE LOGIN mcp_reader WITH PASSWORD = 'Pa55word!2024';

USE erp;
CREATE USER mcp_reader FOR LOGIN mcp_reader;

-- 데이터베이스 전체 읽기 (SQL Server 2012 이상)
ALTER ROLE db_datareader ADD MEMBER mcp_reader;
-- SQL Server 2008 R2 이하: EXEC sp_addrolemember 'db_datareader', 'mcp_reader';
```

> **`db_datareader` 는 그 데이터베이스의 모든 테이블을 열어 줍니다.** 가능하면 아래 3-2 처럼 **필요한 테이블 / view 만** 권한을 주세요.

> ⚠️ **조회 계정이 링크드 서버를 쓸 수 없게 해 주세요.**
> 링크드 서버(다른 SQL Server 나 외부 DB 를 미리 등록해 두고 참조하는 기능) 는 보통 **더 강한 원격 계정**으로 접속하도록 설정돼 있습니다. 그래서 조회 계정에 링크드 서버 접근을 열어 두면, 이 계정에 준 최소권한이 그대로 유지되지 않습니다 (실제 DB 로 확인했습니다). 사내에 링크드 서버가 구성돼 있다면 조회 계정은 그 목록에 매핑하지 마세요.

### 3-2. 노출 범위 좁히기

특정 스키마만:

```sql
USE erp;
-- db_datareader 롤에서 빼야 "DB 전체 읽기" 가 없어집니다 (SQL Server 2012 이상)
ALTER ROLE db_datareader DROP MEMBER mcp_reader;
-- SQL Server 2008 R2 이하: EXEC sp_droprolemember 'db_datareader', 'mcp_reader';
GRANT SELECT ON SCHEMA::dbo TO mcp_reader;
```

> ⚠️ **`REVOKE SELECT FROM mcp_reader;` 로는 `db_datareader` 가 빠지지 않습니다.**
> `db_datareader` 는 롤 **멤버십**이라 `REVOKE` 대상이 아닙니다. 위 `ALTER ROLE ... DROP MEMBER` 를 쓰지 않으면 계정은 계속 **DB 전체를 읽을 수 있고**, Studio 의 권한 자동 점검도 "전체 조회 가능" 으로 계속 표시합니다.
>
> 확인 방법 — 조회 계정으로 접속해 아래를 실행하면 `0` 이어야 정상입니다.
>
> ```sql
> SELECT IS_ROLEMEMBER('db_datareader');  -- 0 이어야 정상 (1 이면 아직 멤버)
> ```

특정 테이블 / view 만:

```sql
GRANT SELECT ON dbo.orders TO mcp_reader;
GRANT SELECT ON dbo.v_summary TO mcp_reader;
```

### 3-3. 연결 정보

| 필드     | 값                                                     |
| -------- | ------------------------------------------------------ |
| Host     | `mssql-erp.internal`                                   |
| Port     | `1433`                                                 |
| Database | `erp`                                                  |
| User     | `mcp_reader`                                           |
| 비밀번호 | `${MSSQL_PASS}`                                        |
| SSL      | `사용 (CA 검증 없음)` 또는 `사용 안함` (사내 네트워크) |

### 3-4. secrets 파일

`/etc/mcp-connector/secrets/mssql.env`:

```
MSSQL_PASS=Pa55word!2024
```

> 비밀번호에 `@` 가 들어가면 SQL Server 연결 문자열에서 따옴표 escape 가 까다로움. **가능하면 `@` 제외한 강력한 비밀번호** 사용.

### 3-5. 사전 연결 테스트

```bash
# sqlcmd (microsoft/mssql-tools 이미지) 또는 사내 환경의 SQL 도구
docker run --rm -it mcr.microsoft.com/mssql-tools \
  /opt/mssql-tools/bin/sqlcmd \
  -S mssql-erp.internal,1433 -U mcp_reader -P 'Pa55word!2024' \
  -d erp -Q "SELECT 1"
```

### 3-6. 트러블슈팅

| 증상                                 | 원인 / 해결                                                         |
| ------------------------------------ | ------------------------------------------------------------------- |
| `Login failed for user 'mcp_reader'` | SQL Server 인증 모드 (Windows / Mixed) 확인 — Mixed Mode 필수       |
| `Cannot open database "erp"`         | 사용자가 그 DB 의 사용자로 추가 안 됨 — `USE erp; CREATE USER` 다시 |
| TLS handshake (encrypted connection) | SQL Server 2019+ 는 default 암호화. Studio SSL 옵션 `require` 권장  |
| `SELECT ... TOP 100` 외 결과 잘림    | 정상 동작 — `execute_query` 가 100 행 제한 (안전 측면)              |

---

## 4. Oracle DB

### 4-1. 읽기 전용 사용자 생성

```sql
-- DBA 로 접속
CREATE USER mcp_reader IDENTIFIED BY "Pa55word!2024"
  DEFAULT TABLESPACE users
  TEMPORARY TABLESPACE temp
  QUOTA 0 ON users;

GRANT CREATE SESSION TO mcp_reader;

-- 특정 스키마 / 테이블 SELECT 권한
GRANT SELECT ON erp_owner.orders TO mcp_reader;
GRANT SELECT ON erp_owner.customers TO mcp_reader;
GRANT SELECT ON erp_owner.v_summary TO mcp_reader;
```

> ⚠️ **조회 계정에 `EXECUTE` (함수·프로시저 실행 권한) 는 주지 마세요.**
> Oracle 도 기본적으로 조회 계정에 실행 권한을 주지 않습니다 — **이 기본값을 그대로 두세요.** (실제 DB 로 확인했습니다: `SELECT` 만 가진 계정은 함수의 존재조차 보이지 않았습니다.)
> Oracle 함수는 바깥 작업과 **독립적으로 자기 변경만 따로 확정하는 기능** (`PRAGMA AUTONOMOUS_TRANSACTION`) 을 쓸 수 있습니다. 이런 함수에 실행 권한을 주면 조회 계정을 통해 데이터가 바뀔 수 있고, 이 경우 제품 쪽에서는 막을 방법이 없어 **계정 권한이 유일한 안전장치**입니다.

> **AI 에게 보이는 목록 = 위에서 `GRANT SELECT` 해 주신 것뿐입니다.**
> 스키마 정보(`describe_schema`)에는 이 계정이 **조회 권한을 받은 테이블·view 만** 나타납니다. 권한을 주지 않은 테이블은 목록에도 없고 조회도 되지 않으니, 위 `GRANT SELECT` 목록이 곧 AI 가 보는 범위입니다. 목록에 오라클이 스스로 관리하는 시스템 스키마는 나오지 않습니다.
> 이름은 `ERP_OWNER.ORDERS` 처럼 **소유자 이름이 앞에 붙어** 표시됩니다 (다른 DB 의 `스키마.테이블` 과 같은 형태). SQL 을 직접 쓰실 때도 `erp_owner.orders` 처럼 소유자를 붙여 주세요.
> 필요한 컬럼만 담은 view 를 `erp_owner` 쪽에 만들어 두고 그 view 에만 권한을 주시면 **노출 범위를 더 좁힐 수 있습니다** (공통 원칙 3).

### 4-2. SERVICE_NAME 확인 (중요)

Oracle 은 **SID 가 아닌 SERVICE_NAME** 사용:

```sql
SELECT value FROM v$parameter WHERE name = 'service_names';
-- 또는
SHOW PARAMETER service_names;
```

출력 예: `ERPSVC`, `XEPDB1`, `FREEPDB1` 등

### 4-3. 연결 정보

| 필드     | 값                                                            |
| -------- | ------------------------------------------------------------- |
| Host     | `oracle-erp.internal`                                         |
| Port     | `1521`                                                        |
| Database | **SERVICE_NAME** (위에서 확인한 값)                           |
| User     | `mcp_reader`                                                  |
| 비밀번호 | `${ORACLE_PASS}`                                              |
| SSL      | `사용 안함` (사내 네트워크) 또는 `사용 (CA 검증 없음)` (TCPS) |

> 미리보기에 표시되는 baseUrl: `oracle://mcp_reader:${ORACLE_PASS}@oracle-erp.internal:1521/ERPSVC`

### 4-4. secrets 파일

`/etc/mcp-connector/secrets/oracle.env`:

```
ORACLE_PASS=Pa55word!2024
```

### 4-5. 사전 연결 테스트

```bash
# sqlplus 가 있을 때
sqlplus 'mcp_reader/"Pa55word!2024"@//oracle-erp.internal:1521/ERPSVC'
# 비밀번호의 특수문자는 따옴표로 감싸기

# 또는 docker 로
docker run --rm -it gvenzl/oracle-free:23-slim sqlplus \
  'mcp_reader/Pa55word!2024@//oracle-erp.internal:1521/ERPSVC'
```

### 4-6. Oracle 버전 호환

| Oracle 버전                          | 우리 커넥터 호환                           |
| ------------------------------------ | ------------------------------------------ |
| 12c (12.1+) / 18c / 19c / 21c / 23ai | ✅ 호환 (Thin 모드 자동)                   |
| 11g 이하 (2013 이전 release)         | ❌ 별도 Thick 모드 이미지 필요 — 별도 문의 |

> 우리 커넥터는 **`oracledb` v6 Thin 모드** 사용 — Instant Client 설치 불필요. 단 Oracle 11g 이하는 미지원 (보안 패치 EOL 이라 사실상 거의 없음).

### 4-7. 트러블슈팅

| 증상                                              | 원인 / 해결                                                                                                                                    |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `ORA-12154: TNS could not resolve`                | 4-3 의 baseUrl 형식 오류. `//host:port/SERVICE_NAME` 형식 확인                                                                                 |
| `ORA-12541: TNS:no listener`                      | 1521 포트 차단 — 사내 방화벽 / DB 호스트 listener 상태 확인                                                                                    |
| `ORA-01017: invalid username/password`            | 비밀번호 특수문자. secrets 파일에 따옴표 없이 그대로 (escape X)                                                                                |
| `ORA-00942: table or view does not exist`         | GRANT SELECT 누락. erp_owner schema 의 객체는 fully qualified (`erp_owner.orders`) 로 권한 부여                                                |
| `describe_schema` 결과가 비어 있음 (`tables: {}`) | 이 계정에 아직 `SELECT` 권한을 준 테이블·view 가 없습니다 — 위 **4-1** 의 `GRANT SELECT ON erp_owner.<테이블> TO mcp_reader;` 를 확인해 주세요 |
| CLOB / BLOB 컬럼 결과가 중간에 끊김               | 정상 동작 — 아래 "CLOB / BLOB 컬럼 읽기" 참고                                                                                                  |

#### CLOB / BLOB 컬럼 읽기 — 빈 칸은 "비어 있음" 이 아닐 수 있습니다

CLOB(긴 글·메모) · BLOB(파일·이미지) 컬럼은 **기본 8MB 까지만** 읽어 옵니다. 중요한 점은 이 8MB 가 **컬럼 한 칸마다가 아니라 조회 결과 전체를 합쳐서 한 번만** 적용된다는 것입니다.

그래서 **앞쪽 행의 큰 컬럼이 8MB 를 다 써버리면, 그 뒤 행의 큰 컬럼 값은 빈 값으로 옵니다.** 응답에 "일부만 받았음" 표시가 있다면, 빈 칸을 **실제로 비어 있는 데이터로 읽지 마세요** — 상한에 걸려 잘린 것입니다.

권장하는 조회 방법:

```sql
-- 필요한 앞부분만 잘라서 (예: 4,000바이트)
SELECT id, DBMS_LOB.SUBSTR(memo, 4000, 1) AS memo_head FROM erp_owner.notes;

-- 또는 한 번에 한 행만
SELECT memo FROM erp_owner.notes WHERE id = 123;
```

가장 안전한 방법은 아예 `DBMS_LOB.SUBSTR(...)` 로 잘라 둔 컬럼만 담은 view 를 `erp_owner` 쪽에 만들어 두고, 조회 계정에는 원본 테이블 대신 **그 view 에만** `SELECT` 권한을 주는 것입니다.

---

## 5. DB 4종 공통 점검 체크리스트

새 DB 툴킷 만들기 전:

- [ ] 읽기 전용 사용자 생성 (DDL/DML 권한 없음)
- [ ] 노출할 스키마 / 테이블만 GRANT — **필요한 것만**. 이 범위가 곧 영향 범위입니다
- [ ] **함수·프로시저 실행 권한(`EXECUTE`) 을 주지 않음** — PostgreSQL 은 기본값이 열려 있으므로 `REVOKE ... FROM PUBLIC` 을 별도로 실행 (위 1-1)
- [ ] **롤을 거치지 않는 전용 계정** — 권한은 계정에 직접 부여 (자동 점검이 "확인 불가" 로 막히는 것을 방지)
- [ ] PostgreSQL: `pg_read_all_data` / `pg_write_all_data` / `SUPERUSER` / `CREATEROLE` 미부여 (위 1-2)
- [ ] MS SQL Server: 조회 계정에 링크드 서버 접근 미부여 (위 3-1). 범위를 좁혔다면 `SELECT IS_ROLEMEMBER('db_datareader')` 가 `0` 인지 확인 (위 3-2)
- [ ] Oracle: 조회에 필요한 테이블 / view 를 `erp_owner.orders` 처럼 **소유자까지 붙여** `GRANT SELECT` — 이 목록이 곧 AI 에게 보이는 범위입니다 (위 4-1)
- [ ] (권장) 운영 DB 대신 **읽기 전용 복제본**에 연결
- [ ] 비밀번호 강도 충분 (특수문자 포함, `@` 가급적 회피)
- [ ] 커넥터 호스트에서 DB 호스트로 telnet / 사내 SQL 도구로 직접 연결 가능
- [ ] SSL 정책 결정 (사내 사설 인증서 → CA 검증 없음 / 공인 → 검증 사용)
- [ ] secrets 파일 권한 600, 폴더 700
- [ ] Studio 폼의 **비밀번호** 필드 = `${VAR}` placeholder (평문 금지). Host / Database / User 는 정책에 따라 평문 또는 placeholder
- [ ] 사용자 운영 모니터링 — DB 측 audit log 도 같이 기록 권장

---

## 6. AI 뷰 추천 (가상 뷰)

DB 툴킷은 기본으로 두 가지 도구 (`describe_schema` · `execute_query`) 를 제공합니다 — AI 가 스키마를 직접 보고 SQL 을 짜는 **자유 질의** 방식입니다. 여기에 더해, 자주 쓰는 조회를 이름 붙은 **가상 뷰** 도구로 만들 수 있습니다. 두 방식은 함께 쓸 수 있고, 뷰를 추가해도 자유 질의 도구는 그대로 남습니다.

**가상 뷰의 장점**

- 반복 업무에서 매번 SQL 을 새로 짜지 않아 안정적입니다
- "일별 매출 집계" 처럼 업무 언어로 대화할 수 있습니다
- 뷰에 적힌 컬럼만 AI 에 보여주므로 개인정보 노출이 줄어듭니다 (데이터베이스에는 아무것도 만들지 않습니다 — 뷰는 이름 붙은 SQL 일 뿐입니다)

**사용 방법** (Studio → DB 툴킷 상세 → 도구 탭)

1. **AI 뷰 추천 받기** — 업무 설명 한 줄 (선택) 을 넣으면 AI 가 스키마를 분석해 뷰 후보를 제안합니다. 개인정보로 보이는 컬럼 (이메일 · 전화번호 등) 은 기본으로 제외되고, 제외된 컬럼과 사유가 카드에 표시됩니다. 계정에 `SELECT` 권한을 준 테이블이 하나도 없으면 분석할 스키마 정보가 없어 제안할 후보도 나오지 않습니다.
2. **검증 실행** — 후보의 SQL 을 1행만 실제로 실행해 문법 · 컬럼 오류를 미리 확인합니다.
3. **도구로 등록** — 클릭하면 이 뷰가 툴킷의 도구 목록에 추가됩니다.
4. **바로 사용** — 등록하면 끝입니다. 서버의 설정 파일에 복사해 붙여넣는 단계는 없습니다 (커넥터 v0.2.10 부터).

> **커넥터 v0.2.10 이상이 필요합니다.** 그보다 낮으면 Studio 에서 뷰 만들기 버튼이 비활성화되고 안내가 표시됩니다.
>
> **예전에 `db-views.json` 으로 만드신 뷰가 있다면** — v0.2.10 으로 업데이트한 뒤에는 그 도구들이 동작하지 않습니다. 도구 목록에서 **삭제한 뒤 화면에서 다시 만들어** 주세요. 파일은 더 이상 읽지 않으므로 정리하셔도 됩니다.

등록된 뷰의 정의는 도구 목록에서 언제든 확인하고 삭제할 수 있습니다. 참고로 정의는 이런 모양입니다:

```json
{
  "daily_sales_summary": {
    "description": "지정한 기간의 일별 주문 건수와 총 매출액 집계",
    "sql": "SELECT date_trunc('day', ordered_at) AS order_date, count(*) AS order_count, sum(amount) AS total_amount FROM orders WHERE ordered_at BETWEEN $1 AND $2 GROUP BY 1 ORDER BY 1",
    "params": [
      { "name": "start_date", "type": "string", "description": "집계 시작일 (ISO 8601)" },
      { "name": "end_date", "type": "string", "description": "집계 종료일 (ISO 8601)" }
    ]
  }
}
```

**권한 안내** — AI 는 계정에 SELECT 권한이 있는 테이블만 읽을 수 있습니다. 자유 질의든 가상 뷰든 **계정 권한이 마지막 경계선**이므로, 민감 테이블 (급여 · 인사 등) 은 계정 권한에서 아예 제외해 원천 차단하세요 (위 1-1 · 1-2 절 참고).

> **Oracle 은 목록에 소유자 이름이 함께 나옵니다** — 스키마 정보의 이름이 `ERP_OWNER.ORDERS` 처럼 **소유자.테이블** 형태입니다 (위 4-1). 가상 뷰의 SQL 을 직접 쓰실 때도 `erp_owner.orders` 처럼 소유자를 붙여 주세요. 그리고 목록에 보이든 안 보이든 마지막 경계선은 여전히 **계정 권한**입니다.

---

## 7. 문의

- DB 권한 / 사내 정책 협의: 사내 DBA / 보안 담당자
- 도구 설명 정확도 / 호출 빈도 튜닝: WRKS 담당자

> 같이 보세요:
>
> - 툴킷 사용 → [Toolkit 사용 가이드](/guides/toolkit-usage)
> - 커넥터 설치 → [Connector 설치 가이드](/guides/connector-installation)
