Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ SLACK_SIGNING_SECRET=your-slack-signing-secret
SLACK_WEBHOOK_ERROR=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
SLACK_WEBHOOK_EVENT=https://hooks.slack.com/services/YOUR/WEBHOOK/URL

# Gemini AI Configuration
GEMINI_PROJECT_ID=your-gcp-project-id
GEMINI_LOCATION=us-central1
GEMINI_MODEL=gemini-1.5-flash
# Gemini AI Configuration (REST API with API Key)
GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-2.0-flash

# MCP Bridge Configuration
MCP_BRIDGE_URL=http://localhost:3100
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
HELP.md
.mcp.json
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
Expand Down Expand Up @@ -44,3 +45,7 @@ logs

.env*
!.env.example

### MCP Bridge ###
mcp-bridge/node_modules/
mcp-bridge/.env
15 changes: 15 additions & 0 deletions .mcp.json
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"
}
Comment on lines +6 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

[LEVEL: medium] 설정 파일 관리 방식 검토 필요

.mcp.json 파일이 레포지토리에 직접 커밋됩니다. 실제 운영 환경에서 이 파일에 실제 자격 증명이 추가될 경우 보안 위험이 발생할 수 있습니다.

권장 방안:

  1. .mcp.json.gitignore에 추가
  2. .mcp.json.example로 이름 변경하여 템플릿임을 명확히 표시
  3. 또는 환경 변수를 참조하도록 변경
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.mcp.json around lines 6 - 12, The committed .mcp.json contains plaintext DB
credentials (MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_DB) which is
a security risk; remove or stop committing it by adding ".mcp.json" to
.gitignore, rename the checked-in file to ".mcp.json.example" to provide a
template, and modify any startup/config loading code to prefer environment
variables (process.env.MYSQL_*) with fallback to the example only for defaults;
ensure secrets are loaded from real env/secret store and update documentation to
show using .mcp.json.example or env vars.

}
}
}
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ dependencies {
// notification
implementation 'com.google.firebase:firebase-admin:9.2.0'

// Gemini AI
implementation 'com.google.cloud:google-cloud-vertexai:1.15.0'
// Gemini AI - using REST API directly (no SDK dependency)

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
9 changes: 9 additions & 0 deletions mcp-bridge/.env.example
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
292 changes: 292 additions & 0 deletions mcp-bridge/index.js
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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
});
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 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.');
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp-bridge/index.js` around lines 64 - 73, The current mcpProcess.on('exit')
handler unconditionally calls setTimeout(startMcpServer, 1000) causing potential
infinite rapid restarts; modify this to implement an exponential/backoff retry
and a max retry count: introduce a restart counter and backoff state (e.g.,
mcpRestartCount, mcpBackoffMs) scoped alongside startMcpServer, increment
mcpRestartCount on each exit and compute delay = min(maxBackoffMs, baseMs *
2**mcpRestartCount), and stop attempting restarts (reject remaining
pendingRequests with a clear error) once maxRestarts is reached; reset
mcpRestartCount/backoff when startMcpServer successfully starts to avoid
permanent backoff.


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
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/query에서 read-only 검증이 SELECT로 시작하는지만 확인하고 있어, SELECT ... INTO OUTFILE, 다중 statement(세미콜론) 등 위험한 형태를 충분히 차단하지 못합니다. Java 쪽(McpClient)의 금지 키워드/형태 검증과 동일한 수준으로 서버 측에서도 방어 로직을 강화해 주세요(직접 브릿지에 접근하는 경우를 대비).

Copilot uses AI. Check for mistakes.
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

[LEVEL: medium] tableName 파라미터 검증 누락

req.params.tableName이 검증 없이 MCP 서버로 전달됩니다. 악의적인 입력(예: 경로 탐색 문자 또는 특수 문자)이 MCP 서버로 전달될 수 있습니다.

🛡️ 테이블 이름 검증 추가 제안
+// 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
Verify each finding against the current code and only fix it if needed.

In `@mcp-bridge/index.js` around lines 206 - 217, The route handler for
app.get('/tables/:tableName') passes req.params.tableName to sendRequest without
validation; validate and sanitize tableName in that handler (before calling
sendRequest) by enforcing a strict allowlist pattern (e.g., only letters,
digits, and underscores like /^[A-Za-z0-9_]+$/), reject or return 400 for
anything that doesn't match, and optionally trim/limit length; reference the
route handler and the sendRequest call (name: 'describe_table', arguments: {
table: req.params.tableName }) so you modify the parameter check right before
that sendRequest invocation to prevent path traversal or special character
injection.


// 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'));
Loading