-
Notifications
You must be signed in to change notification settings - Fork 1
refactor: Slack AI에 MCP 적용 #349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "mcpServers": { | ||
| "mysql": { | ||
| "command": "npx", | ||
| "args": ["@ahngbeom/mysql-mcp-server"], | ||
| "env": { | ||
| "MYSQL_HOST": "localhost", | ||
| "MYSQL_PORT": "3306", | ||
| "MYSQL_USER": "your_user", | ||
| "MYSQL_PASS": "your_password", | ||
| "MYSQL_DB": "your_database" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # MCP Bridge Server Configuration | ||
| MCP_BRIDGE_PORT=3100 | ||
|
|
||
| # MySQL Connection (Read-Only Account Recommended) | ||
| MYSQL_HOST=localhost | ||
| MYSQL_PORT=3306 | ||
| MYSQL_USER=konect_readonly | ||
| MYSQL_PASS=your_password_here | ||
| MYSQL_DB=konect |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,292 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const express = require('express'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { spawn } = require('child_process'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = require('path'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = express(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(express.json()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PORT = process.env.MCP_BRIDGE_PORT || 3100; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const HOST = process.env.MCP_BRIDGE_HOST || '127.0.0.1'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mcpProcess = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let requestId = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pendingRequests = new Map(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let buffer = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Restart backoff strategy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let restartAttempts = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_RESTART_ATTEMPTS = 5; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const BASE_RESTART_DELAY = 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Forbidden SQL keywords for read-only validation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const FORBIDDEN_PATTERNS = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bINSERT\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bUPDATE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bDELETE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bDROP\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bCREATE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bALTER\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bTRUNCATE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bGRANT\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bREVOKE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bEXEC\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bEXECUTE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bINTO\s+OUTFILE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bINTO\s+DUMPFILE\b/i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /;\s*\w/i // Multiple statements | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Valid table name pattern | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const VALID_TABLE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Start MCP server process with backoff | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function startMcpServer() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (restartAttempts >= MAX_RESTART_ATTEMPTS) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Max restart attempts (${MAX_RESTART_ATTEMPTS}) reached. Giving up.`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mcpServerPath = path.join(__dirname, 'node_modules', '@ahngbeom', 'mysql-mcp-server', 'dist', 'index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpProcess = spawn('node', [mcpServerPath], { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...process.env, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MYSQL_HOST: process.env.MYSQL_HOST || 'localhost', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MYSQL_PORT: process.env.MYSQL_PORT || '3306', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MYSQL_USER: process.env.MYSQL_USER, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MYSQL_PASS: process.env.MYSQL_PASS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MYSQL_DB: process.env.MYSQL_DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stdio: ['pipe', 'pipe', 'pipe'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpProcess.stdout.on('data', (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buffer += data.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Process complete JSON-RPC messages (newline delimited) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lines = buffer.split('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buffer = lines.pop() || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const line of lines) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!line.trim()) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = JSON.parse(line); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pending = pendingRequests.get(response.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pending) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending.reject(new Error(response.error.message || 'MCP error')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending.resolve(response.result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRequests.delete(response.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to parse MCP response:', e.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpProcess.stderr.on('data', (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('MCP stderr:', data.toString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpProcess.on('exit', (code) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`MCP process exited with code ${code}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reject all pending requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [id, pending] of pendingRequests) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending.reject(new Error('MCP process exited')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRequests.delete(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Exponential backoff restart | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| restartAttempts++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const delay = BASE_RESTART_DELAY * Math.pow(2, restartAttempts - 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Attempting restart ${restartAttempts}/${MAX_RESTART_ATTEMPTS} in ${delay}ms...`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(startMcpServer, delay); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+96
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial [LEVEL: medium] MCP 프로세스 무한 재시작 가능성 MCP 프로세스가 지속적으로 실패하면 1초 간격으로 무한 재시작되어 리소스 낭비 및 로그 폭주가 발생할 수 있습니다. 백오프 전략 또는 최대 재시작 횟수 제한을 권장합니다. 💡 재시작 백오프 로직 제안+let restartAttempts = 0;
+const MAX_RESTART_ATTEMPTS = 5;
+const BASE_RESTART_DELAY = 1000;
+
mcpProcess.on('exit', (code) => {
console.log(`MCP process exited with code ${code}`);
// Reject all pending requests
for (const [id, pending] of pendingRequests) {
pending.reject(new Error('MCP process exited'));
pendingRequests.delete(id);
}
- // Restart after delay
- setTimeout(startMcpServer, 1000);
+ // Restart with exponential backoff
+ restartAttempts++;
+ if (restartAttempts <= MAX_RESTART_ATTEMPTS) {
+ const delay = BASE_RESTART_DELAY * Math.pow(2, restartAttempts - 1);
+ console.log(`Restarting MCP in ${delay}ms (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})`);
+ setTimeout(startMcpServer, delay);
+ } else {
+ console.error('Max restart attempts reached. MCP server will not restart.');
+ }
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('MCP server process started'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| restartAttempts = 0; // Reset on successful start | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Initialize MCP connection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendRequest('initialize', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protocolVersion: '2024-11-05', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| capabilities: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientInfo: { name: 'konect-mcp-bridge', version: '1.0.0' } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).then(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('MCP connection initialized'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sendRequest('notifications/initialized', {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).catch(err => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('MCP initialization failed:', err.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Send JSON-RPC request to MCP server | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function sendRequest(method, params) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mcpProcess || mcpProcess.killed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject(new Error('MCP process not running')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const id = ++requestId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timeout = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRequests.delete(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject(new Error('Request timeout')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 30000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRequests.set(id, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve: (result) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(timeout); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve(result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject: (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(timeout); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const request = JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jsonrpc: '2.0', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpProcess.stdin.write(request + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validate SQL is read-only | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateReadOnlySql(sql) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!sql || typeof sql !== 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { valid: false, error: 'SQL is required' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trimmedSql = sql.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!trimmedSql.toUpperCase().startsWith('SELECT')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { valid: false, error: 'Only SELECT queries are allowed' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const pattern of FORBIDDEN_PATTERNS) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pattern.test(trimmedSql)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { valid: false, error: 'Query contains forbidden pattern' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { valid: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validate table name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateTableName(tableName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!tableName || typeof tableName !== 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return VALID_TABLE_NAME.test(tableName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Health check endpoint | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get('/health', (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isHealthy = mcpProcess && !mcpProcess.killed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(isHealthy ? 200 : 503).json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: isHealthy ? 'healthy' : 'unhealthy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpRunning: isHealthy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Convenience endpoint for SQL queries | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.post('/query', async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { sql } = req.body; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validation = validateReadOnlySql(sql); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!validation.valid) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: validation.error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'READ_ONLY_VIOLATION' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+207
to
+217
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await sendRequest('tools/call', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'query', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arguments: { sql } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.json(result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Query execution failed:', err.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(500).json({ error: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // List tables endpoint | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get('/tables', async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await sendRequest('tools/call', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'list_tables', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arguments: {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.json(result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to list tables:', err.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(500).json({ error: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Describe table endpoint | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get('/tables/:tableName', async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { tableName } = req.params; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!validateTableName(tableName)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'Invalid table name', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'INVALID_TABLE_NAME' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await sendRequest('tools/call', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'describe_table', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arguments: { table: tableName } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.json(result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Failed to describe table ${tableName}:`, err.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(500).json({ error: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+245
to
+265
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LEVEL: medium]
🛡️ 테이블 이름 검증 추가 제안+// Validate table name (alphanumeric and underscore only)
+function isValidTableName(name) {
+ return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
+}
+
// Describe table endpoint
app.get('/tables/:tableName', async (req, res) => {
+ const { tableName } = req.params;
+ if (!isValidTableName(tableName)) {
+ return res.status(400).json({ error: 'Invalid table name' });
+ }
+
try {
const result = await sendRequest('tools/call', {
name: 'describe_table',
- arguments: { table: req.params.tableName }
+ arguments: { table: tableName }
});🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Start server | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| startMcpServer(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.listen(PORT, HOST, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`MCP Bridge server running on ${HOST}:${PORT}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Graceful shutdown | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function gracefulShutdown(signal) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Received ${signal}, shutting down...`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reject all pending requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [id, pending] of pendingRequests) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending.reject(new Error('Server shutting down')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRequests.delete(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (mcpProcess) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcpProcess.kill(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.on('SIGINT', () => gracefulShutdown('SIGINT')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[LEVEL: medium] 설정 파일 관리 방식 검토 필요
.mcp.json파일이 레포지토리에 직접 커밋됩니다. 실제 운영 환경에서 이 파일에 실제 자격 증명이 추가될 경우 보안 위험이 발생할 수 있습니다.권장 방안:
.mcp.json을.gitignore에 추가.mcp.json.example로 이름 변경하여 템플릿임을 명확히 표시🤖 Prompt for AI Agents