redash-mcp 2.1.1 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/README.ko.md +142 -0
- package/README.md +86 -47
- package/dist/index.js +43 -3
- package/dist/query-cache.js +74 -0
- package/dist/sql-guard.js +159 -0
- package/docs/sql-safety-guard.md +197 -0
- package/package.json +9 -1
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
package-lock.json linguist-generated=true
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# redash-mcp
|
|
2
|
+
|
|
3
|
+
[Redash](https://redash.io)를 Claude AI에 연결하는 MCP 서버 — 자연어로 데이터를 조회하고 대시보드를 관리하세요.
|
|
4
|
+
|
|
5
|
+
**[English Documentation](README.md)**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 기능
|
|
10
|
+
|
|
11
|
+
### 툴 목록
|
|
12
|
+
|
|
13
|
+
| 카테고리 | 툴 | 설명 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 데이터소스 | `list_data_sources` | 연결된 데이터소스 목록 조회 |
|
|
16
|
+
| 스키마 | `list_tables` | 테이블 목록 조회 (키워드 검색 가능) |
|
|
17
|
+
| 스키마 | `get_table_columns` | 테이블 컬럼명 및 타입 조회 |
|
|
18
|
+
| 쿼리 실행 | `run_query` | SQL 직접 실행 후 결과 반환 |
|
|
19
|
+
| 저장 쿼리 | `list_queries` | 저장된 쿼리 목록 조회 |
|
|
20
|
+
| 저장 쿼리 | `get_query` | 쿼리 상세 정보 (SQL, 시각화 등) 조회 |
|
|
21
|
+
| 저장 쿼리 | `get_query_result` | 저장된 쿼리 실행 결과 조회 |
|
|
22
|
+
| 저장 쿼리 | `create_query` | 새 쿼리 저장 |
|
|
23
|
+
| 저장 쿼리 | `update_query` | 쿼리 수정 |
|
|
24
|
+
| 저장 쿼리 | `fork_query` | 쿼리 복제 |
|
|
25
|
+
| 저장 쿼리 | `archive_query` | 쿼리 삭제 (아카이브) |
|
|
26
|
+
| 대시보드 | `list_dashboards` | 대시보드 목록 조회 |
|
|
27
|
+
| 대시보드 | `get_dashboard` | 대시보드 상세 및 위젯 목록 조회 |
|
|
28
|
+
| 대시보드 | `create_dashboard` | 새 대시보드 생성 |
|
|
29
|
+
| 대시보드 | `add_widget` | 대시보드에 시각화 위젯 추가 |
|
|
30
|
+
| 알림 | `list_alerts` | 알림 목록 조회 |
|
|
31
|
+
| 알림 | `get_alert` | 알림 상세 정보 조회 |
|
|
32
|
+
| 알림 | `create_alert` | 새 알림 생성 |
|
|
33
|
+
|
|
34
|
+
### SQL 안전 가드
|
|
35
|
+
|
|
36
|
+
위험한 쿼리로부터 데이터베이스를 보호합니다:
|
|
37
|
+
|
|
38
|
+
- **항상 차단**: `DROP`, `TRUNCATE`, `ALTER TABLE`, `GRANT/REVOKE`, `WHERE` 없는 `DELETE/UPDATE`
|
|
39
|
+
- **경고 (warn 모드)** / **차단 (strict 모드)**: `SELECT *`, `WHERE`·`LIMIT` 없는 쿼리, PII 컬럼 접근
|
|
40
|
+
- **자동 LIMIT**: `REDASH_AUTO_LIMIT` 설정 시 LIMIT 없는 쿼리에 자동으로 `LIMIT N` 추가
|
|
41
|
+
|
|
42
|
+
### 쿼리 캐시
|
|
43
|
+
|
|
44
|
+
중복 API 호출을 줄이기 위해 결과를 메모리에 캐싱합니다:
|
|
45
|
+
|
|
46
|
+
- TTL: `REDASH_MCP_CACHE_TTL` 환경변수로 설정 (기본값: 300초)
|
|
47
|
+
- 최대 메모리: `REDASH_MCP_CACHE_MAX_MB` 환경변수로 설정 (기본값: 50MB)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 설치
|
|
52
|
+
|
|
53
|
+
### 자동 설치 (권장)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx redash-mcp setup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
설치 마법사가 실행되며 Claude Desktop, Claude Code(CLI), 또는 둘 다 선택하여 설정할 수 있습니다.
|
|
60
|
+
|
|
61
|
+
### 수동 설치
|
|
62
|
+
|
|
63
|
+
#### 1. Redash API 키 발급
|
|
64
|
+
|
|
65
|
+
Redash → 우측 상단 프로필 → **Edit Profile** → **API Key** 복사
|
|
66
|
+
|
|
67
|
+
#### 2-A. Claude Desktop 설정
|
|
68
|
+
|
|
69
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` 파일을 열고 아래 내용을 추가합니다:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"redash-mcp": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "redash-mcp"],
|
|
77
|
+
"env": {
|
|
78
|
+
"REDASH_URL": "https://your-redash-instance.com",
|
|
79
|
+
"REDASH_API_KEY": "your_api_key_here"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
저장 후 Claude Desktop을 완전히 종료했다가 다시 시작합니다.
|
|
87
|
+
|
|
88
|
+
#### 2-B. Claude Code (CLI) 설정
|
|
89
|
+
|
|
90
|
+
`~/.claude/settings.json` 파일을 열고 아래 내용을 추가합니다:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"redash-mcp": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["-y", "redash-mcp"],
|
|
98
|
+
"env": {
|
|
99
|
+
"REDASH_URL": "https://your-redash-instance.com",
|
|
100
|
+
"REDASH_API_KEY": "your_api_key_here"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> **macOS**: `npx`를 못 찾는 경우 `which npx` 명령어로 전체 경로를 확인 후 대체하세요.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 환경 변수
|
|
112
|
+
|
|
113
|
+
### 필수
|
|
114
|
+
|
|
115
|
+
| 변수 | 설명 |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `REDASH_URL` | Redash 인스턴스 URL (예: `https://redash.example.com`) |
|
|
118
|
+
| `REDASH_API_KEY` | Redash 사용자 API 키 |
|
|
119
|
+
|
|
120
|
+
### 선택
|
|
121
|
+
|
|
122
|
+
| 변수 | 기본값 | 설명 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `REDASH_SAFETY_MODE` | `warn` | SQL 안전 수준: `off` / `warn` / `strict` |
|
|
125
|
+
| `REDASH_SAFETY_DISABLE_PII` | `false` | PII 감지 비활성화 |
|
|
126
|
+
| `REDASH_SAFETY_DISABLE_COST` | `false` | 비용 경고 비활성화 |
|
|
127
|
+
| `REDASH_AUTO_LIMIT` | `0` | LIMIT 없는 쿼리에 자동으로 `LIMIT N` 추가 (0 = 비활성화) |
|
|
128
|
+
| `REDASH_DEFAULT_MAX_AGE` | `0` | Redash 캐시 TTL (초) |
|
|
129
|
+
| `REDASH_MCP_CACHE_TTL` | `300` | MCP 쿼리 캐시 TTL (초, 0 = 비활성화) |
|
|
130
|
+
| `REDASH_MCP_CACHE_MAX_MB` | `50` | MCP 쿼리 캐시 최대 메모리 (MB) |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 사용 예시
|
|
135
|
+
|
|
136
|
+
Claude에게 자연어로 요청하면 됩니다:
|
|
137
|
+
|
|
138
|
+
- "users 테이블 컬럼 보여줘"
|
|
139
|
+
- "최근 7일 주문 수를 SQL로 조회해줘"
|
|
140
|
+
- "저장된 쿼리 목록 보여줘"
|
|
141
|
+
- "매출 대시보드 위젯 목록 알려줘"
|
|
142
|
+
- "일별 가입자 수가 100명 이하로 떨어지면 알림 만들어줘"
|
package/README.md
CHANGED
|
@@ -1,49 +1,72 @@
|
|
|
1
1
|
# redash-mcp
|
|
2
2
|
|
|
3
|
-
[Redash](https://redash.io)
|
|
3
|
+
MCP server that connects [Redash](https://redash.io) to Claude AI — query data, manage dashboards, and run SQL with natural language.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**[한국어 문서](README.ko.md)**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Tools
|
|
12
|
+
|
|
13
|
+
| Category | Tool | Description |
|
|
8
14
|
|---|---|---|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
| Data Sources | `list_data_sources` | List connected data sources |
|
|
16
|
+
| Schema | `list_tables` | List tables (supports keyword search) |
|
|
17
|
+
| Schema | `get_table_columns` | Get column names and types |
|
|
18
|
+
| Query | `run_query` | Execute SQL and return results |
|
|
19
|
+
| Saved Queries | `list_queries` | List saved queries |
|
|
20
|
+
| Saved Queries | `get_query` | Get query details (SQL, visualizations) |
|
|
21
|
+
| Saved Queries | `get_query_result` | Run a saved query and get results |
|
|
22
|
+
| Saved Queries | `create_query` | Save a new query |
|
|
23
|
+
| Saved Queries | `update_query` | Update a saved query |
|
|
24
|
+
| Saved Queries | `fork_query` | Fork a saved query |
|
|
25
|
+
| Saved Queries | `archive_query` | Archive (delete) a query |
|
|
26
|
+
| Dashboards | `list_dashboards` | List dashboards |
|
|
27
|
+
| Dashboards | `get_dashboard` | Get dashboard details and widgets |
|
|
28
|
+
| Dashboards | `create_dashboard` | Create a new dashboard |
|
|
29
|
+
| Dashboards | `add_widget` | Add a visualization widget to a dashboard |
|
|
30
|
+
| Alerts | `list_alerts` | List alerts |
|
|
31
|
+
| Alerts | `get_alert` | Get alert details |
|
|
32
|
+
| Alerts | `create_alert` | Create a new alert |
|
|
33
|
+
|
|
34
|
+
### SQL Safety Guard
|
|
35
|
+
|
|
36
|
+
Protects your database from dangerous queries:
|
|
37
|
+
|
|
38
|
+
- **Blocked always**: `DROP`, `TRUNCATE`, `ALTER TABLE`, `GRANT/REVOKE`, `DELETE/UPDATE` without `WHERE`
|
|
39
|
+
- **Warned (warn mode)** / **Blocked (strict mode)**: `SELECT *`, queries without `WHERE` or `LIMIT`, PII column access
|
|
40
|
+
- **Auto-LIMIT**: Automatically appends `LIMIT N` when `REDASH_AUTO_LIMIT` is set
|
|
41
|
+
|
|
42
|
+
### Query Cache
|
|
43
|
+
|
|
44
|
+
Results are cached in-memory to reduce redundant API calls:
|
|
45
|
+
|
|
46
|
+
- TTL: configurable via `REDASH_MCP_CACHE_TTL` (default: 300s)
|
|
47
|
+
- Max memory: configurable via `REDASH_MCP_CACHE_MAX_MB` (default: 50MB)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### Auto Setup (Recommended)
|
|
31
54
|
|
|
32
55
|
```bash
|
|
33
56
|
npx redash-mcp setup
|
|
34
57
|
```
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
The setup wizard will guide you through configuring Claude Desktop, Claude Code (CLI), or both.
|
|
37
60
|
|
|
38
|
-
###
|
|
61
|
+
### Manual Setup
|
|
39
62
|
|
|
40
|
-
#### 1. Redash API
|
|
63
|
+
#### 1. Get your Redash API Key
|
|
41
64
|
|
|
42
|
-
Redash →
|
|
65
|
+
Go to Redash → Profile (top right) → **Edit Profile** → Copy **API Key**
|
|
43
66
|
|
|
44
|
-
#### 2-A. Claude Desktop
|
|
67
|
+
#### 2-A. Claude Desktop
|
|
45
68
|
|
|
46
|
-
`~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
69
|
+
Open `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
|
|
47
70
|
|
|
48
71
|
```json
|
|
49
72
|
{
|
|
@@ -60,11 +83,11 @@ Redash → 우측 상단 프로필 → **Edit Profile** → **API Key** 복사
|
|
|
60
83
|
}
|
|
61
84
|
```
|
|
62
85
|
|
|
63
|
-
|
|
86
|
+
Fully quit and restart Claude Desktop after saving.
|
|
64
87
|
|
|
65
|
-
#### 2-B. Claude Code (CLI)
|
|
88
|
+
#### 2-B. Claude Code (CLI)
|
|
66
89
|
|
|
67
|
-
`~/.claude/settings.json`
|
|
90
|
+
Open `~/.claude/settings.json` and add:
|
|
68
91
|
|
|
69
92
|
```json
|
|
70
93
|
{
|
|
@@ -81,23 +104,39 @@ Redash → 우측 상단 프로필 → **Edit Profile** → **API Key** 복사
|
|
|
81
104
|
}
|
|
82
105
|
```
|
|
83
106
|
|
|
84
|
-
|
|
107
|
+
> **macOS**: If `npx` is not found, run `which npx` to get the full path and use that instead.
|
|
85
108
|
|
|
86
|
-
|
|
87
|
-
> **macOS**: `npx`를 못 찾는 경우 `which npx` 명령어로 전체 경로를 확인 후 대체하세요.
|
|
109
|
+
---
|
|
88
110
|
|
|
89
|
-
##
|
|
111
|
+
## Environment Variables
|
|
90
112
|
|
|
91
|
-
|
|
113
|
+
### Required
|
|
114
|
+
|
|
115
|
+
| Variable | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `REDASH_URL` | Redash instance URL (e.g. `https://redash.example.com`) |
|
|
118
|
+
| `REDASH_API_KEY` | Redash user API key |
|
|
119
|
+
|
|
120
|
+
### Optional
|
|
121
|
+
|
|
122
|
+
| Variable | Default | Description |
|
|
92
123
|
|---|---|---|
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
124
|
+
| `REDASH_SAFETY_MODE` | `warn` | SQL safety level: `off` / `warn` / `strict` |
|
|
125
|
+
| `REDASH_SAFETY_DISABLE_PII` | `false` | Disable PII detection |
|
|
126
|
+
| `REDASH_SAFETY_DISABLE_COST` | `false` | Disable cost warnings |
|
|
127
|
+
| `REDASH_AUTO_LIMIT` | `0` | Auto-append `LIMIT N` to queries without one (0 = disabled) |
|
|
128
|
+
| `REDASH_DEFAULT_MAX_AGE` | `0` | Redash cache TTL in seconds |
|
|
129
|
+
| `REDASH_MCP_CACHE_TTL` | `300` | MCP query cache TTL in seconds (0 = disabled) |
|
|
130
|
+
| `REDASH_MCP_CACHE_MAX_MB` | `50` | Max memory for MCP query cache in MB |
|
|
131
|
+
|
|
132
|
+
---
|
|
95
133
|
|
|
96
|
-
##
|
|
134
|
+
## Usage Examples
|
|
97
135
|
|
|
98
|
-
Claude
|
|
136
|
+
Just ask Claude in natural language:
|
|
99
137
|
|
|
100
|
-
- "
|
|
101
|
-
- "
|
|
102
|
-
- "
|
|
103
|
-
- "
|
|
138
|
+
- "Show me the columns in the users table"
|
|
139
|
+
- "Run a query to get order counts for the last 7 days"
|
|
140
|
+
- "List all saved queries"
|
|
141
|
+
- "Show widgets in the revenue dashboard"
|
|
142
|
+
- "Create an alert when daily signups drop below 100"
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { analyzeQuery } from "./sql-guard.js";
|
|
6
|
+
import { getCached, setCached } from "./query-cache.js";
|
|
5
7
|
if (process.argv[2] === "setup") {
|
|
6
8
|
const { main } = await import("./setup.js");
|
|
7
9
|
await main();
|
|
@@ -127,17 +129,52 @@ server.tool("get_table_columns", "테이블의 컬럼명과 타입을 반환합
|
|
|
127
129
|
};
|
|
128
130
|
});
|
|
129
131
|
// ─── Query Execution ──────────────────────────────────────────────────────────
|
|
132
|
+
const DEFAULT_MAX_AGE = parseInt(process.env.REDASH_DEFAULT_MAX_AGE ?? "0", 10) || 0;
|
|
130
133
|
server.tool("run_query", "SQL을 데이터소스에 직접 실행하고 결과를 반환합니다. SQL 작성 전 list_tables → get_table_columns로 스키마를 먼저 확인하세요.", {
|
|
131
134
|
data_source_id: z.number().describe("list_data_sources로 확인한 데이터소스 ID"),
|
|
132
135
|
query: z.string().describe("실행할 SQL 쿼리"),
|
|
133
|
-
max_age: z.number().optional().
|
|
136
|
+
max_age: z.number().optional().describe("Redash 캐시 유지 시간(초). 미지정 시 REDASH_DEFAULT_MAX_AGE 환경변수 사용"),
|
|
134
137
|
max_rows: z.number().optional().default(100).describe("반환할 최대 행 수 (기본 100)"),
|
|
135
138
|
format: z.enum(["table", "json"]).optional().default("table").describe("결과 포맷: table(마크다운) 또는 json"),
|
|
136
139
|
timeout_secs: z.number().optional().default(30).describe("쿼리 실행 타임아웃(초)"),
|
|
137
140
|
}, async ({ data_source_id, query, max_age, max_rows, format, timeout_secs }) => {
|
|
141
|
+
// 1. SQL 안전 가드
|
|
142
|
+
const guard = analyzeQuery(query);
|
|
143
|
+
if (guard.blocked) {
|
|
144
|
+
return { content: [{ type: "text", text: guard.message }] };
|
|
145
|
+
}
|
|
146
|
+
// auto-LIMIT이 적용된 경우 변환된 쿼리 사용
|
|
147
|
+
const effectiveQuery = guard.modifiedQuery ?? query;
|
|
148
|
+
const effectiveMaxAge = max_age ?? DEFAULT_MAX_AGE;
|
|
149
|
+
// 2. MCP 레이어 캐시 조회
|
|
150
|
+
const cached = getCached(data_source_id, effectiveQuery);
|
|
151
|
+
if (cached) {
|
|
152
|
+
const { rows, columns, warningPrefix } = cached;
|
|
153
|
+
const displayRows = rows.slice(0, max_rows);
|
|
154
|
+
const truncated = rows.length > max_rows
|
|
155
|
+
? `\n⚠️ 전체 ${rows.length}행 중 ${max_rows}행만 표시합니다.`
|
|
156
|
+
: "";
|
|
157
|
+
let body;
|
|
158
|
+
if (format === "json") {
|
|
159
|
+
body = JSON.stringify(displayRows, null, 2);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
body = formatAsMarkdownTable(columns, displayRows);
|
|
163
|
+
}
|
|
164
|
+
const cacheNote = "📦 MCP 캐시에서 반환된 결과입니다.\n\n";
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `${warningPrefix}${cacheNote}총 ${rows.length}행 | 컬럼: ${columns.join(", ")}${truncated}\n\n${body}`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// 3. Redash API 호출
|
|
138
175
|
const res = await redashFetch("/query_results", {
|
|
139
176
|
method: "POST",
|
|
140
|
-
body: JSON.stringify({ data_source_id, query, max_age }),
|
|
177
|
+
body: JSON.stringify({ data_source_id, query: effectiveQuery, max_age: effectiveMaxAge }),
|
|
141
178
|
});
|
|
142
179
|
let result;
|
|
143
180
|
if (res.job) {
|
|
@@ -149,6 +186,9 @@ server.tool("run_query", "SQL을 데이터소스에 직접 실행하고 결과
|
|
|
149
186
|
const qr = result.query_result;
|
|
150
187
|
const rows = qr.data.rows;
|
|
151
188
|
const columns = qr.data.columns.map((c) => c.name);
|
|
189
|
+
// 4. MCP 캐시에 저장
|
|
190
|
+
const warningPrefix = guard.message ? `${guard.message}\n\n` : "";
|
|
191
|
+
setCached(data_source_id, effectiveQuery, { rows, columns, warningPrefix });
|
|
152
192
|
const displayRows = rows.slice(0, max_rows);
|
|
153
193
|
const truncated = rows.length > max_rows
|
|
154
194
|
? `\n⚠️ 전체 ${rows.length}행 중 ${max_rows}행만 표시합니다.`
|
|
@@ -164,7 +204,7 @@ server.tool("run_query", "SQL을 데이터소스에 직접 실행하고 결과
|
|
|
164
204
|
content: [
|
|
165
205
|
{
|
|
166
206
|
type: "text",
|
|
167
|
-
text:
|
|
207
|
+
text: `${warningPrefix}총 ${rows.length}행 | 컬럼: ${columns.join(", ")}${truncated}\n\n${body}`,
|
|
168
208
|
},
|
|
169
209
|
],
|
|
170
210
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
let totalSizeBytes = 0;
|
|
4
|
+
function getCacheTtlMs() {
|
|
5
|
+
const ttl = parseInt(process.env.REDASH_MCP_CACHE_TTL ?? "300", 10);
|
|
6
|
+
return (isNaN(ttl) ? 300 : ttl) * 1000;
|
|
7
|
+
}
|
|
8
|
+
function getMaxSizeBytes() {
|
|
9
|
+
const mb = parseInt(process.env.REDASH_MCP_CACHE_MAX_MB ?? "50", 10);
|
|
10
|
+
return (isNaN(mb) ? 50 : mb) * 1024 * 1024;
|
|
11
|
+
}
|
|
12
|
+
function normalizeSQL(sql) {
|
|
13
|
+
return sql
|
|
14
|
+
.replace(/--[^\n]*/g, " ")
|
|
15
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
16
|
+
.replace(/\s+/g, " ")
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
function makeCacheKey(dataSourceId, sql) {
|
|
21
|
+
return createHash("sha256")
|
|
22
|
+
.update(`${dataSourceId}:${normalizeSQL(sql)}`)
|
|
23
|
+
.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
function roughSize(obj) {
|
|
26
|
+
return JSON.stringify(obj).length * 2;
|
|
27
|
+
}
|
|
28
|
+
export function getCached(dataSourceId, sql) {
|
|
29
|
+
const ttl = getCacheTtlMs();
|
|
30
|
+
if (ttl === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const key = makeCacheKey(dataSourceId, sql);
|
|
33
|
+
const entry = cache.get(key);
|
|
34
|
+
if (!entry)
|
|
35
|
+
return null;
|
|
36
|
+
if (Date.now() - entry.ts > ttl) {
|
|
37
|
+
totalSizeBytes -= entry.size;
|
|
38
|
+
cache.delete(key);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return entry.result;
|
|
42
|
+
}
|
|
43
|
+
export function setCached(dataSourceId, sql, result) {
|
|
44
|
+
const ttl = getCacheTtlMs();
|
|
45
|
+
if (ttl === 0)
|
|
46
|
+
return;
|
|
47
|
+
const maxSize = getMaxSizeBytes();
|
|
48
|
+
const size = roughSize(result);
|
|
49
|
+
// 단일 결과가 전체 한도의 20% 초과 시 캐시하지 않음
|
|
50
|
+
if (size > maxSize * 0.2)
|
|
51
|
+
return;
|
|
52
|
+
// 한도 초과 시 오래된 항목부터 제거
|
|
53
|
+
while (totalSizeBytes + size > maxSize && cache.size > 0) {
|
|
54
|
+
const oldestKey = cache.keys().next().value;
|
|
55
|
+
if (!oldestKey)
|
|
56
|
+
break;
|
|
57
|
+
const old = cache.get(oldestKey);
|
|
58
|
+
totalSizeBytes -= old.size;
|
|
59
|
+
cache.delete(oldestKey);
|
|
60
|
+
}
|
|
61
|
+
const key = makeCacheKey(dataSourceId, sql);
|
|
62
|
+
const existing = cache.get(key);
|
|
63
|
+
if (existing)
|
|
64
|
+
totalSizeBytes -= existing.size;
|
|
65
|
+
cache.set(key, { result, ts: Date.now(), size });
|
|
66
|
+
totalSizeBytes += size;
|
|
67
|
+
}
|
|
68
|
+
export function getCacheStats() {
|
|
69
|
+
return {
|
|
70
|
+
entries: cache.size,
|
|
71
|
+
sizeMb: (totalSizeBytes / 1024 / 1024).toFixed(2),
|
|
72
|
+
ttlSecs: getCacheTtlMs() / 1000,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
function getConfig() {
|
|
2
|
+
const raw = process.env.REDASH_SAFETY_MODE ?? "warn";
|
|
3
|
+
const mode = ["off", "warn", "strict"].includes(raw) ? raw : "warn";
|
|
4
|
+
return {
|
|
5
|
+
mode,
|
|
6
|
+
disablePii: process.env.REDASH_SAFETY_DISABLE_PII === "true",
|
|
7
|
+
disableCost: process.env.REDASH_SAFETY_DISABLE_COST === "true",
|
|
8
|
+
autoLimit: parseInt(process.env.REDASH_AUTO_LIMIT ?? "0", 10) || 0,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function normalizeForAnalysis(sql) {
|
|
12
|
+
return sql
|
|
13
|
+
.replace(/--[^\n]*/g, " ")
|
|
14
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
15
|
+
.replace(/\s+/g, " ")
|
|
16
|
+
.trim()
|
|
17
|
+
.toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
function hasWhere(sql) {
|
|
20
|
+
return /\bWHERE\b/.test(sql);
|
|
21
|
+
}
|
|
22
|
+
function hasLimit(sql) {
|
|
23
|
+
return /\bLIMIT\b/.test(sql);
|
|
24
|
+
}
|
|
25
|
+
function isSelect(sql) {
|
|
26
|
+
return /^\s*(SELECT|WITH)\b/i.test(sql);
|
|
27
|
+
}
|
|
28
|
+
function injectLimit(sql, limit) {
|
|
29
|
+
if (/\bLIMIT\b/i.test(sql))
|
|
30
|
+
return sql;
|
|
31
|
+
if (!isSelect(sql))
|
|
32
|
+
return sql;
|
|
33
|
+
return `${sql.trimEnd()} LIMIT ${limit}`;
|
|
34
|
+
}
|
|
35
|
+
export function analyzeQuery(sql) {
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
if (config.mode === "off") {
|
|
38
|
+
return { blocked: false, warnings: [], message: "" };
|
|
39
|
+
}
|
|
40
|
+
const upper = normalizeForAnalysis(sql);
|
|
41
|
+
const warnings = [];
|
|
42
|
+
let modifiedQuery;
|
|
43
|
+
// ── Destructive (차단: warn/strict 모두) ────────────────────────────────────
|
|
44
|
+
if (/\bDROP\s+(TABLE|DATABASE|SCHEMA|VIEW|INDEX|FUNCTION)\b/.test(upper)) {
|
|
45
|
+
return {
|
|
46
|
+
blocked: true,
|
|
47
|
+
warnings: [],
|
|
48
|
+
message: "🚫 쿼리가 차단되었습니다.\n\n사유: DROP 문은 데이터/스키마를 영구 삭제합니다.\n규칙: DESTRUCTIVE / DROP\n\n해제가 필요하다면 REDASH_SAFETY_MODE=off 로 설정하세요.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (/\bTRUNCATE\b/.test(upper)) {
|
|
52
|
+
return {
|
|
53
|
+
blocked: true,
|
|
54
|
+
warnings: [],
|
|
55
|
+
message: "🚫 쿼리가 차단되었습니다.\n\n사유: TRUNCATE 문은 전체 테이블 데이터를 삭제합니다.\n규칙: DESTRUCTIVE / TRUNCATE",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (/\bALTER\s+TABLE\b/.test(upper)) {
|
|
59
|
+
return {
|
|
60
|
+
blocked: true,
|
|
61
|
+
warnings: [],
|
|
62
|
+
message: "🚫 쿼리가 차단되었습니다.\n\n사유: ALTER TABLE은 스키마 변경으로 사전 협의가 필요합니다.\n규칙: DESTRUCTIVE / ALTER_TABLE",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (/\b(GRANT|REVOKE)\b/.test(upper)) {
|
|
66
|
+
return {
|
|
67
|
+
blocked: true,
|
|
68
|
+
warnings: [],
|
|
69
|
+
message: "🚫 쿼리가 차단되었습니다.\n\n사유: GRANT/REVOKE는 권한 변경으로 허용되지 않습니다.\n규칙: DESTRUCTIVE / PRIVILEGE_CHANGE",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (/\bDELETE\s+FROM\b/.test(upper) && !hasWhere(upper)) {
|
|
73
|
+
return {
|
|
74
|
+
blocked: true,
|
|
75
|
+
warnings: [],
|
|
76
|
+
message: "🚫 쿼리가 차단되었습니다.\n\n사유: WHERE 조건 없는 DELETE는 전체 데이터를 삭제합니다.\n규칙: DESTRUCTIVE / DELETE_WITHOUT_WHERE\n\n안전한 예시:\n DELETE FROM orders WHERE created_at < '2024-01-01'",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (/\bUPDATE\b/.test(upper) && /\bSET\b/.test(upper) && !hasWhere(upper)) {
|
|
80
|
+
return {
|
|
81
|
+
blocked: true,
|
|
82
|
+
warnings: [],
|
|
83
|
+
message: "🚫 쿼리가 차단되었습니다.\n\n사유: WHERE 조건 없는 UPDATE는 전체 데이터를 수정합니다.\n규칙: DESTRUCTIVE / UPDATE_WITHOUT_WHERE\n\n안전한 예시:\n UPDATE orders SET status = 'cancelled' WHERE created_at < '2024-01-01'",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// DELETE/UPDATE with WHERE — 경고만
|
|
87
|
+
if (/\bDELETE\s+FROM\b/.test(upper)) {
|
|
88
|
+
warnings.push("[DESTRUCTIVE] DELETE 쿼리입니다. WHERE 조건을 다시 확인하세요.");
|
|
89
|
+
}
|
|
90
|
+
if (/\bUPDATE\b/.test(upper) && /\bSET\b/.test(upper)) {
|
|
91
|
+
warnings.push("[DESTRUCTIVE] UPDATE 쿼리입니다. WHERE 조건을 다시 확인하세요.");
|
|
92
|
+
}
|
|
93
|
+
// ── Cost (warn: 경고 후 실행, strict: 차단) ────────────────────────────────
|
|
94
|
+
if (!config.disableCost && isSelect(sql)) {
|
|
95
|
+
const hasSelectStar = /SELECT\s+\*/.test(upper) || /SELECT\s+[\w.]+\.\*/.test(upper);
|
|
96
|
+
const noWhere = !hasWhere(upper);
|
|
97
|
+
const noLimit = !hasLimit(upper);
|
|
98
|
+
if (hasSelectStar) {
|
|
99
|
+
warnings.push("[COST] SELECT *를 사용하고 있습니다. 필요한 컬럼만 명시하면 BigQuery 스캔 비용을 줄일 수 있습니다.");
|
|
100
|
+
}
|
|
101
|
+
if (noWhere) {
|
|
102
|
+
warnings.push("[COST] WHERE 조건이 없습니다. 날짜 또는 조건 필터를 추가하는 것을 권장합니다.");
|
|
103
|
+
}
|
|
104
|
+
if (noLimit) {
|
|
105
|
+
if (config.autoLimit > 0) {
|
|
106
|
+
modifiedQuery = injectLimit(sql, config.autoLimit);
|
|
107
|
+
warnings.push(`[COST] LIMIT이 없어 자동으로 LIMIT ${config.autoLimit}을 추가했습니다. 전체 조회가 필요하면 명시적으로 LIMIT을 지정하세요.`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
warnings.push("[COST] LIMIT이 없습니다. 대용량 테이블에서는 전체 데이터가 반환되어 비용이 발생할 수 있습니다.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (config.mode === "strict") {
|
|
114
|
+
const costWarnings = warnings.filter((w) => w.startsWith("[COST]"));
|
|
115
|
+
if (costWarnings.length > 0) {
|
|
116
|
+
return {
|
|
117
|
+
blocked: true,
|
|
118
|
+
warnings: [],
|
|
119
|
+
message: `🚫 쿼리가 차단되었습니다 (strict 모드).\n\n${costWarnings.join("\n")}\n\nwarn 모드로 변경하려면 REDASH_SAFETY_MODE=warn 으로 설정하세요.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── PII (warn: 경고 후 실행, strict: 차단) ────────────────────────────────
|
|
125
|
+
if (!config.disablePii) {
|
|
126
|
+
const piiPatterns = [
|
|
127
|
+
"EMAIL",
|
|
128
|
+
"PHONE",
|
|
129
|
+
"PASSWORD",
|
|
130
|
+
"PASSWD",
|
|
131
|
+
"SSN",
|
|
132
|
+
"SOCIAL_SECURITY",
|
|
133
|
+
"CREDIT_CARD",
|
|
134
|
+
"CARD_NUMBER",
|
|
135
|
+
"주민",
|
|
136
|
+
"휴대폰",
|
|
137
|
+
"핸드폰",
|
|
138
|
+
"생년월일",
|
|
139
|
+
];
|
|
140
|
+
const matched = piiPatterns.filter((k) => upper.includes(k));
|
|
141
|
+
if (matched.length > 0) {
|
|
142
|
+
warnings.push(`[PII] 민감 정보 관련 컬럼이 감지되었습니다: ${matched.join(", ")}. 개인정보 처리 규정을 확인하세요.`);
|
|
143
|
+
}
|
|
144
|
+
if (config.mode === "strict") {
|
|
145
|
+
const piiWarnings = warnings.filter((w) => w.startsWith("[PII]"));
|
|
146
|
+
if (piiWarnings.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
blocked: true,
|
|
149
|
+
warnings: [],
|
|
150
|
+
message: `🚫 쿼리가 차단되었습니다 (strict 모드).\n\n${piiWarnings.join("\n")}\n\nwarn 모드로 변경하려면 REDASH_SAFETY_MODE=warn 으로 설정하세요.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const message = warnings.length > 0
|
|
156
|
+
? `⚠️ 안전 경고 (쿼리는 실행됩니다)\n\n${warnings.join("\n")}\n\n---`
|
|
157
|
+
: "";
|
|
158
|
+
return { blocked: false, warnings, message, modifiedQuery };
|
|
159
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# SQL Safety Guard — 설계 문서
|
|
2
|
+
|
|
3
|
+
## 개요
|
|
4
|
+
|
|
5
|
+
`redash-mcp`에 SQL 안전 가드 레이어를 추가하여 MCP를 통해 실행되는 쿼리로 인한
|
|
6
|
+
비용 발생, 데이터 손실, 개인정보 노출을 사전에 방지한다.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 배경 및 목적
|
|
11
|
+
|
|
12
|
+
- Redash를 MCP로 연결하면 AI가 자동으로 쿼리를 생성·실행하는 환경이 된다
|
|
13
|
+
- 사용자가 SQL을 직접 검토하지 않는 경우가 많아 위험 쿼리가 그대로 실행될 수 있음
|
|
14
|
+
- BigQuery는 스캔한 데이터 용량 기준으로 과금되어 예상치 못한 비용 발생 가능
|
|
15
|
+
- AI가 생성한 쿼리에 DROP, DELETE 등이 포함될 경우 데이터 손실 위험
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 아키텍처
|
|
20
|
+
|
|
21
|
+
### 위치
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
AI (Claude)
|
|
25
|
+
↓ tool call
|
|
26
|
+
run_query (MCP 도구)
|
|
27
|
+
↓
|
|
28
|
+
[SQL Safety Guard] ← 여기에 삽입
|
|
29
|
+
↓ 통과 시
|
|
30
|
+
Redash API
|
|
31
|
+
↓
|
|
32
|
+
BigQuery
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 패턴
|
|
36
|
+
|
|
37
|
+
미들웨어/인터셉터 패턴. `run_query` 핸들러 내부에서 Redash API 호출 전에 실행.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// src/sql-guard.ts
|
|
41
|
+
export function analyzeQuery(sql: string, mode: SafetyMode): SafetyResult
|
|
42
|
+
|
|
43
|
+
// src/index.ts (run_query 핸들러)
|
|
44
|
+
const result = analyzeQuery(query, safetyMode);
|
|
45
|
+
if (result.blocked) {
|
|
46
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
47
|
+
}
|
|
48
|
+
// ... Redash API 호출
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 감지 규칙
|
|
54
|
+
|
|
55
|
+
### Category 1: 데이터 파괴 (Destructive) — 기본: 차단
|
|
56
|
+
|
|
57
|
+
| 규칙 | 패턴 예시 | 기본 동작 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| DDL 변경 | `DROP TABLE`, `TRUNCATE`, `ALTER TABLE` | 차단 |
|
|
60
|
+
| 조건 없는 삭제 | `DELETE FROM table` (WHERE 없음) | 차단 |
|
|
61
|
+
| 조건 없는 수정 | `UPDATE table SET ...` (WHERE 없음) | 차단 |
|
|
62
|
+
| 권한 변경 | `GRANT`, `REVOKE` | 차단 |
|
|
63
|
+
|
|
64
|
+
### Category 2: 비용 위험 (Cost) — 기본: 경고
|
|
65
|
+
|
|
66
|
+
| 규칙 | 패턴 예시 | 기본 동작 |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| WHERE 없는 SELECT | `SELECT ... FROM table` (WHERE 없음) | 경고 |
|
|
69
|
+
| SELECT * | `SELECT * FROM ...` | 경고 |
|
|
70
|
+
| 집계 없는 대량 조회 | 조건 없이 raw 데이터 전체 조회 | 경고 |
|
|
71
|
+
|
|
72
|
+
### Category 3: 개인정보 (PII) — 기본: 경고
|
|
73
|
+
|
|
74
|
+
| 규칙 | 감지 컬럼명 키워드 | 기본 동작 |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| 민감 컬럼 SELECT | `email`, `phone`, `password`, `ssn`, `social`, `주민`, `휴대폰`, `핸드폰` | 경고 |
|
|
77
|
+
|
|
78
|
+
> PII 규칙은 컬럼명 기반 휴리스틱이며 오탐이 발생할 수 있다.
|
|
79
|
+
> 환경변수로 비활성화 가능.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 동작 모드
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
REDASH_SAFETY_MODE=off|warn|strict
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| 모드 | Destructive | Cost | PII |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `off` | 통과 | 통과 | 통과 |
|
|
92
|
+
| `warn` (기본) | **차단** | 경고 후 실행 | 경고 후 실행 |
|
|
93
|
+
| `strict` | **차단** | **차단** | **차단** |
|
|
94
|
+
|
|
95
|
+
- Destructive는 모든 모드에서 `warn` 이상이면 차단 (데이터 손실은 되돌릴 수 없음)
|
|
96
|
+
- `off`는 완전히 비활성화가 필요한 경우를 위해 제공 (내부 관리자용 등)
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 환경변수
|
|
101
|
+
|
|
102
|
+
| 변수 | 기본값 | 설명 |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `REDASH_SAFETY_MODE` | `warn` | 전체 안전 모드 |
|
|
105
|
+
| `REDASH_SAFETY_DISABLE_PII` | `false` | PII 감지 비활성화 |
|
|
106
|
+
| `REDASH_SAFETY_DISABLE_COST` | `false` | 비용 경고 비활성화 |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 응답 포맷
|
|
111
|
+
|
|
112
|
+
### 차단 시
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
🚫 쿼리가 차단되었습니다.
|
|
116
|
+
|
|
117
|
+
사유: WHERE 조건 없이 전체 테이블을 수정하는 쿼리는 실행할 수 없습니다.
|
|
118
|
+
규칙: DESTRUCTIVE / UPDATE_WITHOUT_WHERE
|
|
119
|
+
|
|
120
|
+
안전한 쿼리 예시:
|
|
121
|
+
UPDATE orders SET status = 'cancelled' WHERE created_at < '2024-01-01'
|
|
122
|
+
|
|
123
|
+
안전 모드 해제가 필요하다면 REDASH_SAFETY_MODE=off 로 설정하세요.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 경고 시 (warn 모드)
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
⚠️ 안전 경고 (쿼리는 실행됩니다)
|
|
130
|
+
|
|
131
|
+
[COST] SELECT *를 사용하고 있습니다. 필요한 컬럼만 명시하면 비용을 줄일 수 있습니다.
|
|
132
|
+
[COST] WHERE 조건이 없습니다. 날짜 또는 조건 필터를 추가하는 것을 권장합니다.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
(쿼리 결과)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 파일 구조
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
src/
|
|
144
|
+
index.ts # 기존 — run_query에 가드 연결
|
|
145
|
+
sql-guard.ts # 신규 — 안전 가드 핵심 로직
|
|
146
|
+
setup.ts # 기존 — 설치 마법사에 SAFETY_MODE 설정 추가
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## setup 마법사 변경
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
? BigQuery 안전 모드를 선택하세요
|
|
155
|
+
○ off — 제한 없음 (관리자 전용)
|
|
156
|
+
● warn — 위험 패턴 경고 후 실행 (권장)
|
|
157
|
+
○ strict — 위험 쿼리 차단
|
|
158
|
+
|
|
159
|
+
? 개인정보(PII) 컬럼 감지를 활성화하시겠습니까?
|
|
160
|
+
● 예 / ○ 아니오
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 구현 범위 (v2.2.0)
|
|
166
|
+
|
|
167
|
+
### 포함
|
|
168
|
+
- [x] Destructive 쿼리 차단 (DDL, DELETE/UPDATE without WHERE)
|
|
169
|
+
- [x] Cost 경고/차단 (WHERE 없는 SELECT, SELECT *)
|
|
170
|
+
- [x] PII 컬럼 경고 (컬럼명 기반)
|
|
171
|
+
- [x] `REDASH_SAFETY_MODE` 환경변수
|
|
172
|
+
- [x] setup 마법사 안전 모드 설정 추가
|
|
173
|
+
- [x] 단위 테스트 (vitest)
|
|
174
|
+
|
|
175
|
+
### 제외 (추후 검토)
|
|
176
|
+
- Cartesian JOIN 감지 (SQL 파서 필요, 복잡도 높음)
|
|
177
|
+
- 쿼리 실행 횟수 기반 Rate limiting (상태 관리 필요)
|
|
178
|
+
- 실제 BQ 스캔 바이트 추정 (BQ 직접 접근 불가)
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 공식 플러그인 등록
|
|
183
|
+
|
|
184
|
+
### MCP 공식 레지스트리
|
|
185
|
+
- `modelcontextprotocol/servers` GitHub 레포에 PR 제출
|
|
186
|
+
- 요건: npm 패키지, README, 설치 가이드
|
|
187
|
+
|
|
188
|
+
### Claude Desktop
|
|
189
|
+
- 현재 Claude Desktop 플러그인 = MCP 서버
|
|
190
|
+
- 공식 디렉토리 등록은 Anthropic 신청 필요 (초대제 운영 중)
|
|
191
|
+
- 단기적으로는 MCP 커뮤니티 레지스트리 등록으로 대응
|
|
192
|
+
|
|
193
|
+
### 등록 준비 체크리스트
|
|
194
|
+
- [ ] README 영문화 (설치, 환경변수, 도구 목록)
|
|
195
|
+
- [ ] 라이선스 명시 (MIT)
|
|
196
|
+
- [ ] 버전 관리 전략 (semver)
|
|
197
|
+
- [ ] CHANGELOG 작성
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redash-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "MCP server for Redash",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/seob717/redash-mcp.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/seob717/redash-mcp#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/seob717/redash-mcp/issues"
|
|
12
|
+
},
|
|
5
13
|
"type": "module",
|
|
6
14
|
"main": "dist/index.js",
|
|
7
15
|
"bin": {
|