redash-mcp 2.2.1 → 3.0.0

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/dist/setup.js CHANGED
@@ -38,9 +38,9 @@ function getClaudeCodeConfigPath() {
38
38
  return path.join(os.homedir(), ".claude", "settings.json");
39
39
  }
40
40
  export async function main() {
41
- p.intro("redash-mcp 설치 마법사");
41
+ p.intro("redash-mcp setup wizard");
42
42
  const targets = await p.multiselect({
43
- message: "설치 대상을 선택하세요 (스페이스바로 선택, 엔터로 확인)",
43
+ message: "Select installation targets (space to select, enter to confirm)",
44
44
  options: [
45
45
  { value: "desktop", label: "Claude Desktop" },
46
46
  { value: "cli", label: "Claude Code (CLI)" },
@@ -48,91 +48,32 @@ export async function main() {
48
48
  required: true,
49
49
  });
50
50
  if (p.isCancel(targets)) {
51
- p.cancel("설치가 취소되었습니다.");
51
+ p.cancel("Setup cancelled.");
52
52
  process.exit(0);
53
53
  }
54
54
  const redashUrl = await p.text({
55
- message: "Redash URL을 입력하세요",
55
+ message: "Enter your Redash URL",
56
56
  placeholder: "https://redash.example.com",
57
57
  validate(value) {
58
58
  if (!value)
59
- return "URL 입력해주세요.";
59
+ return "URL is required.";
60
60
  if (!value.startsWith("http://") && !value.startsWith("https://"))
61
- return "http:// 또는 https://로 시작해야 합니다.";
61
+ return "Must start with http:// or https://";
62
62
  },
63
63
  });
64
64
  if (p.isCancel(redashUrl)) {
65
- p.cancel("설치가 취소되었습니다.");
65
+ p.cancel("Setup cancelled.");
66
66
  process.exit(0);
67
67
  }
68
68
  const apiKey = await p.text({
69
- message: "Redash API 키를 입력하세요",
69
+ message: "Enter your Redash API key",
70
70
  validate(value) {
71
71
  if (!value)
72
- return "API 키를 입력해주세요.";
72
+ return "API key is required.";
73
73
  },
74
74
  });
75
75
  if (p.isCancel(apiKey)) {
76
- p.cancel("설치가 취소되었습니다.");
77
- process.exit(0);
78
- }
79
- // ── 안전 모드 설정 ────────────────────────────────────────────────────────
80
- const safetyMode = await p.select({
81
- message: "SQL 안전 모드를 선택하세요",
82
- options: [
83
- { value: "warn", label: "warn — 위험 쿼리 경고 후 실행 (권장)" },
84
- { value: "strict", label: "strict — 위험 쿼리 차단" },
85
- { value: "off", label: "off — 제한 없음 (관리자 전용)" },
86
- ],
87
- initialValue: "warn",
88
- });
89
- if (p.isCancel(safetyMode)) {
90
- p.cancel("설치가 취소되었습니다.");
91
- process.exit(0);
92
- }
93
- const autoLimitRaw = await p.text({
94
- message: "자동 LIMIT 값을 입력하세요 (0 = 비활성화)",
95
- placeholder: "1000",
96
- initialValue: "1000",
97
- validate(value) {
98
- if (!value)
99
- return undefined;
100
- if (isNaN(parseInt(value, 10)))
101
- return "숫자를 입력해주세요.";
102
- },
103
- });
104
- if (p.isCancel(autoLimitRaw)) {
105
- p.cancel("설치가 취소되었습니다.");
106
- process.exit(0);
107
- }
108
- const defaultMaxAgeRaw = await p.text({
109
- message: "Redash 캐시 유지 시간을 입력하세요 (초, 0 = 항상 새로 실행)",
110
- placeholder: "600",
111
- initialValue: "600",
112
- validate(value) {
113
- if (!value)
114
- return undefined;
115
- if (isNaN(parseInt(value, 10)))
116
- return "숫자를 입력해주세요.";
117
- },
118
- });
119
- if (p.isCancel(defaultMaxAgeRaw)) {
120
- p.cancel("설치가 취소되었습니다.");
121
- process.exit(0);
122
- }
123
- const mcpCacheTtlRaw = await p.text({
124
- message: "MCP 레이어 캐시 TTL을 입력하세요 (초, 0 = 비활성화)",
125
- placeholder: "300",
126
- initialValue: "300",
127
- validate(value) {
128
- if (!value)
129
- return undefined;
130
- if (isNaN(parseInt(value, 10)))
131
- return "숫자를 입력해주세요.";
132
- },
133
- });
134
- if (p.isCancel(mcpCacheTtlRaw)) {
135
- p.cancel("설치가 취소되었습니다.");
76
+ p.cancel("Setup cancelled.");
136
77
  process.exit(0);
137
78
  }
138
79
  const url = redashUrl.replace(/\/$/, "");
@@ -143,24 +84,20 @@ export async function main() {
143
84
  env: {
144
85
  REDASH_URL: url,
145
86
  REDASH_API_KEY: apiKey,
146
- REDASH_SAFETY_MODE: String(safetyMode),
147
- REDASH_AUTO_LIMIT: String(autoLimitRaw || "1000"),
148
- REDASH_DEFAULT_MAX_AGE: String(defaultMaxAgeRaw || "600"),
149
- REDASH_MCP_CACHE_TTL: String(mcpCacheTtlRaw || "300"),
150
87
  },
151
88
  };
152
89
  const s = p.spinner();
153
90
  if (targets.includes("desktop")) {
154
- s.start("Claude Desktop 설정 중...");
91
+ s.start("Configuring Claude Desktop...");
155
92
  setupDesktop(mcpEntry);
156
- s.stop("Claude Desktop 설정 완료");
93
+ s.stop("Claude Desktop configured");
157
94
  }
158
95
  if (targets.includes("cli")) {
159
- s.start("Claude Code (CLI) 설정 중...");
96
+ s.start("Configuring Claude Code (CLI)...");
160
97
  setupClaudeCode(mcpEntry);
161
- s.stop("Claude Code (CLI) 설정 완료");
98
+ s.stop("Claude Code (CLI) configured");
162
99
  }
163
- p.outro("설치가 완료되었습니다. 재시작 사용할 있습니다.");
100
+ p.outro("Setup complete. Restart to start using redash-mcp.");
164
101
  }
165
102
  function setupDesktop(mcpEntry) {
166
103
  const configPath = getDesktopConfigPath();
@@ -171,7 +108,7 @@ function setupDesktop(mcpEntry) {
171
108
  config.mcpServers ??= {};
172
109
  }
173
110
  catch {
174
- throw new Error(`claude_desktop_config.json 파일을 읽을 수 없습니다: ${configPath}`);
111
+ throw new Error(`Failed to read claude_desktop_config.json: ${configPath}`);
175
112
  }
176
113
  }
177
114
  else {
@@ -188,7 +125,7 @@ function setupClaudeCode(mcpEntry) {
188
125
  config = JSON.parse(fs.readFileSync(configPath, "utf8"));
189
126
  }
190
127
  catch {
191
- throw new Error(`Claude Code settings.json 파일을 읽을 수 없습니다: ${configPath}`);
128
+ throw new Error(`Failed to read Claude Code settings.json: ${configPath}`);
192
129
  }
193
130
  }
194
131
  else {
package/dist/sql-guard.js CHANGED
@@ -40,74 +40,71 @@ export function analyzeQuery(sql) {
40
40
  const upper = normalizeForAnalysis(sql);
41
41
  const warnings = [];
42
42
  let modifiedQuery;
43
- // ── Destructive (차단: warn/strict 모두) ────────────────────────────────────
44
43
  if (/\bDROP\s+(TABLE|DATABASE|SCHEMA|VIEW|INDEX|FUNCTION)\b/.test(upper)) {
45
44
  return {
46
45
  blocked: true,
47
46
  warnings: [],
48
- message: "🚫 쿼리가 차단되었습니다.\n\n사유: DROP 문은 데이터/스키마를 영구 삭제합니다.\n규칙: DESTRUCTIVE / DROP\n\n해제가 필요하다면 REDASH_SAFETY_MODE=off 설정하세요.",
47
+ message: "🚫 Query blocked.\n\nReason: DROP statements permanently delete data/schema.\nRule: DESTRUCTIVE / DROP\n\nSet REDASH_SAFETY_MODE=off to disable this check.",
49
48
  };
50
49
  }
51
50
  if (/\bTRUNCATE\b/.test(upper)) {
52
51
  return {
53
52
  blocked: true,
54
53
  warnings: [],
55
- message: "🚫 쿼리가 차단되었습니다.\n\n사유: TRUNCATE 문은 전체 테이블 데이터를 삭제합니다.\n규칙: DESTRUCTIVE / TRUNCATE",
54
+ message: "🚫 Query blocked.\n\nReason: TRUNCATE deletes all data from the table.\nRule: DESTRUCTIVE / TRUNCATE",
56
55
  };
57
56
  }
58
57
  if (/\bALTER\s+TABLE\b/.test(upper)) {
59
58
  return {
60
59
  blocked: true,
61
60
  warnings: [],
62
- message: "🚫 쿼리가 차단되었습니다.\n\n사유: ALTER TABLE 스키마 변경으로 사전 협의가 필요합니다.\n규칙: DESTRUCTIVE / ALTER_TABLE",
61
+ message: "🚫 Query blocked.\n\nReason: ALTER TABLE modifies schema and requires prior coordination.\nRule: DESTRUCTIVE / ALTER_TABLE",
63
62
  };
64
63
  }
65
64
  if (/\b(GRANT|REVOKE)\b/.test(upper)) {
66
65
  return {
67
66
  blocked: true,
68
67
  warnings: [],
69
- message: "🚫 쿼리가 차단되었습니다.\n\n사유: GRANT/REVOKE 권한 변경으로 허용되지 않습니다.\n규칙: DESTRUCTIVE / PRIVILEGE_CHANGE",
68
+ message: "🚫 Query blocked.\n\nReason: GRANT/REVOKE permission changes are not allowed.\nRule: DESTRUCTIVE / PRIVILEGE_CHANGE",
70
69
  };
71
70
  }
72
71
  if (/\bDELETE\s+FROM\b/.test(upper) && !hasWhere(upper)) {
73
72
  return {
74
73
  blocked: true,
75
74
  warnings: [],
76
- message: "🚫 쿼리가 차단되었습니다.\n\n사유: WHERE 조건 없는 DELETE는 전체 데이터를 삭제합니다.\n규칙: DESTRUCTIVE / DELETE_WITHOUT_WHERE\n\n안전한 예시:\n DELETE FROM orders WHERE created_at < '2024-01-01'",
75
+ message: "🚫 Query blocked.\n\nReason: DELETE without WHERE clause will delete all rows.\nRule: DESTRUCTIVE / DELETE_WITHOUT_WHERE\n\nSafe example:\n DELETE FROM orders WHERE created_at < '2024-01-01'",
77
76
  };
78
77
  }
79
78
  if (/\bUPDATE\b/.test(upper) && /\bSET\b/.test(upper) && !hasWhere(upper)) {
80
79
  return {
81
80
  blocked: true,
82
81
  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'",
82
+ message: "🚫 Query blocked.\n\nReason: UPDATE without WHERE clause will modify all rows.\nRule: DESTRUCTIVE / UPDATE_WITHOUT_WHERE\n\nSafe example:\n UPDATE orders SET status = 'cancelled' WHERE created_at < '2024-01-01'",
84
83
  };
85
84
  }
86
- // DELETE/UPDATE with WHERE — 경고만
87
85
  if (/\bDELETE\s+FROM\b/.test(upper)) {
88
- warnings.push("[DESTRUCTIVE] DELETE 쿼리입니다. WHERE 조건을 다시 확인하세요.");
86
+ warnings.push("[DESTRUCTIVE] DELETE query detected. Please verify the WHERE clause.");
89
87
  }
90
88
  if (/\bUPDATE\b/.test(upper) && /\bSET\b/.test(upper)) {
91
- warnings.push("[DESTRUCTIVE] UPDATE 쿼리입니다. WHERE 조건을 다시 확인하세요.");
89
+ warnings.push("[DESTRUCTIVE] UPDATE query detected. Please verify the WHERE clause.");
92
90
  }
93
- // ── Cost (warn: 경고 후 실행, strict: 차단) ────────────────────────────────
94
91
  if (!config.disableCost && isSelect(sql)) {
95
92
  const hasSelectStar = /SELECT\s+\*/.test(upper) || /SELECT\s+[\w.]+\.\*/.test(upper);
96
93
  const noWhere = !hasWhere(upper);
97
94
  const noLimit = !hasLimit(upper);
98
95
  if (hasSelectStar) {
99
- warnings.push("[COST] SELECT *를 사용하고 있습니다. 필요한 컬럼만 명시하면 BigQuery 스캔 비용을 줄일 수 있습니다.");
96
+ warnings.push("[COST] SELECT * detected. Specify only needed columns to reduce scan costs.");
100
97
  }
101
98
  if (noWhere) {
102
- warnings.push("[COST] WHERE 조건이 없습니다. 날짜 또는 조건 필터를 추가하는 것을 권장합니다.");
99
+ warnings.push("[COST] No WHERE clause. Consider adding date or condition filters.");
103
100
  }
104
101
  if (noLimit) {
105
102
  if (config.autoLimit > 0) {
106
103
  modifiedQuery = injectLimit(sql, config.autoLimit);
107
- warnings.push(`[COST] LIMIT 없어 자동으로 LIMIT ${config.autoLimit} 추가했습니다. 전체 조회가 필요하면 명시적으로 LIMIT을 지정하세요.`);
104
+ warnings.push(`[COST] No LIMIT clause auto-appended LIMIT ${config.autoLimit}. Specify an explicit LIMIT if you need all rows.`);
108
105
  }
109
106
  else {
110
- warnings.push("[COST] LIMIT 없습니다. 대용량 테이블에서는 전체 데이터가 반환되어 비용이 발생할 있습니다.");
107
+ warnings.push("[COST] No LIMIT clause. Full table scans on large tables may incur significant costs.");
111
108
  }
112
109
  }
113
110
  if (config.mode === "strict") {
@@ -116,12 +113,11 @@ export function analyzeQuery(sql) {
116
113
  return {
117
114
  blocked: true,
118
115
  warnings: [],
119
- message: `🚫 쿼리가 차단되었습니다 (strict 모드).\n\n${costWarnings.join("\n")}\n\nwarn 모드로 변경하려면 REDASH_SAFETY_MODE=warn 으로 설정하세요.`,
116
+ message: `🚫 Query blocked (strict mode).\n\n${costWarnings.join("\n")}\n\nSet REDASH_SAFETY_MODE=warn to allow with warnings.`,
120
117
  };
121
118
  }
122
119
  }
123
120
  }
124
- // ── PII (warn: 경고 후 실행, strict: 차단) ────────────────────────────────
125
121
  if (!config.disablePii) {
126
122
  const piiPatterns = [
127
123
  "EMAIL",
@@ -132,14 +128,10 @@ export function analyzeQuery(sql) {
132
128
  "SOCIAL_SECURITY",
133
129
  "CREDIT_CARD",
134
130
  "CARD_NUMBER",
135
- "주민",
136
- "휴대폰",
137
- "핸드폰",
138
- "생년월일",
139
131
  ];
140
132
  const matched = piiPatterns.filter((k) => upper.includes(k));
141
133
  if (matched.length > 0) {
142
- warnings.push(`[PII] 민감 정보 관련 컬럼이 감지되었습니다: ${matched.join(", ")}. 개인정보 처리 규정을 확인하세요.`);
134
+ warnings.push(`[PII] Sensitive data columns detected: ${matched.join(", ")}. Please verify your data privacy compliance.`);
143
135
  }
144
136
  if (config.mode === "strict") {
145
137
  const piiWarnings = warnings.filter((w) => w.startsWith("[PII]"));
@@ -147,13 +139,13 @@ export function analyzeQuery(sql) {
147
139
  return {
148
140
  blocked: true,
149
141
  warnings: [],
150
- message: `🚫 쿼리가 차단되었습니다 (strict 모드).\n\n${piiWarnings.join("\n")}\n\nwarn 모드로 변경하려면 REDASH_SAFETY_MODE=warn 으로 설정하세요.`,
142
+ message: `🚫 Query blocked (strict mode).\n\n${piiWarnings.join("\n")}\n\nSet REDASH_SAFETY_MODE=warn to allow with warnings.`,
151
143
  };
152
144
  }
153
145
  }
154
146
  }
155
147
  const message = warnings.length > 0
156
- ? `⚠️ 안전 경고 (쿼리는 실행됩니다)\n\n${warnings.join("\n")}\n\n---`
148
+ ? `⚠️ Safety warnings (query will still execute)\n\n${warnings.join("\n")}\n\n---`
157
149
  : "";
158
150
  return { blocked: false, warnings, message, modifiedQuery };
159
151
  }
package/manifest.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "manifest_version": "0.3",
3
+ "name": "redash-mcp",
4
+ "version": "3.0.0",
5
+ "description": "MCP server for Redash -- query data, manage dashboards, and run SQL with natural language. 24 tools including BIRD SQL methodology.",
6
+ "author": {
7
+ "name": "seob717",
8
+ "email": "dev.seob717@gmail.com"
9
+ },
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/seob717/redash-mcp"
14
+ },
15
+ "privacy_policies": [
16
+ "https://github.com/seob717/redash-mcp#privacy-policy"
17
+ ],
18
+ "support": "https://github.com/seob717/redash-mcp/issues",
19
+ "server": {
20
+ "type": "node",
21
+ "entry_point": "dist/index.js",
22
+ "mcp_config": {
23
+ "command": "npx",
24
+ "args": ["-y", "redash-mcp"],
25
+ "env": {
26
+ "REDASH_URL": "${user_config.redash_url}",
27
+ "REDASH_API_KEY": "${user_config.redash_api_key}"
28
+ }
29
+ }
30
+ },
31
+ "user_config": {
32
+ "redash_url": {
33
+ "type": "string",
34
+ "title": "Redash URL",
35
+ "description": "Your Redash instance URL (e.g. https://redash.example.com)",
36
+ "required": true
37
+ },
38
+ "redash_api_key": {
39
+ "type": "string",
40
+ "title": "Redash API Key",
41
+ "description": "Your Redash user API key (found in Settings > Account)",
42
+ "sensitive": true,
43
+ "required": true
44
+ }
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,14 +1,27 @@
1
1
  {
2
2
  "name": "redash-mcp",
3
- "version": "2.2.1",
3
+ "version": "3.0.0",
4
4
  "description": "MCP server for Redash",
5
+ "license": "MIT",
6
+ "author": "seob717",
7
+ "keywords": [
8
+ "mcp",
9
+ "redash",
10
+ "sql",
11
+ "bi",
12
+ "dashboard",
13
+ "model-context-protocol"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
5
18
  "repository": {
6
19
  "type": "git",
7
- "url": "https://github.com/seob717/redash-mcp.git"
20
+ "url": "https://github.com/jiro-developers/redash-mcp.git"
8
21
  },
9
- "homepage": "https://github.com/seob717/redash-mcp#readme",
22
+ "homepage": "https://github.com/jiro-developers/redash-mcp#readme",
10
23
  "bugs": {
11
- "url": "https://github.com/seob717/redash-mcp/issues"
24
+ "url": "https://github.com/jiro-developers/redash-mcp/issues"
12
25
  },
13
26
  "type": "module",
14
27
  "main": "dist/index.js",
@@ -21,6 +34,7 @@
21
34
  "start": "node dist/index.js"
22
35
  },
23
36
  "dependencies": {
37
+ "@anthropic-ai/sdk": "^0.80.0",
24
38
  "@clack/prompts": "^1.1.0",
25
39
  "@modelcontextprotocol/sdk": "^1.0.0",
26
40
  "zod": "^3.0.0"
package/.gitattributes DELETED
@@ -1 +0,0 @@
1
- package-lock.json linguist-generated=true
@@ -1,197 +0,0 @@
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 작성