본문 바로가기
chatGPT와 인공지능AI

AI로 홈페이지 만들기 - SQLITE MCP 연결

by Cray Fall 2025. 7. 6.

바이브 코딩 홈페이지 만들기 시리즈 이어서 나갑니다.
지난 시간에는 회원가입/로그인을 연동했었는데요.
이번 시간에는 향후 더 편리한 작업을 위해 sqlite MCP ( 한글판 by 크레이 ) 연결을 할겁니다.

https://itadventure.tistory.com/763

 

AI로 홈페이지 회원가입/로그인 만들기

이제부터는 본격적인 홈페이지 동적 기능을 만들 차례입니다.홈페이지 개발과정에 가장 먼저 구현해야 하는 것은 무엇일까요?개발자마다 의견이 분분할 수 있지만 저는 '회원가입/로그인' 기능

itadventure.tistory.com

이후로도 계속해서 Windsurf의 기본 기능으로 지시하면 이것 저것 해주긴 하지만,
그 과정에 불필요한 소스 파일들을 많이 생성하는 문제가 있었습니다.

그에 대한 좋은 해결책이 있는데요.
뭐냐면 바로 'MCP 서버'입니다.

IT 지식 나갑니다~

MCP 란 '모델 컨텍스트 프로토콜'이란 건데요.
쉬운 말로 AI가 문맥을 파악해 서비스를 사용하게 해주는 것입니다.
재미있는 것은 문맥에 '자연어'를 사용한다는 것이지요.

MCP 서버를 만드는 것은 쉽지 않은 일입니다.
그래서 완성된 단일 소스 코드를 드릴테니 이 소스코드를 여러분의 XAMPP 에 심어주시면 되는데요.
먼저 윈도우 탐색기에서 아래 폴더를 생성해 주세요.C:\xampp\htdocs\sqlitemcp

Windsurf 를 실행하여 위 폴더를 선택하신 다음에
1) 새 파일
2) sqlite_mcp.php 파일을 생성해주세요.

그리고 아래 소스 코드를 복붙하고 저장하세요.

<?php
// PHP SQLite MCP 서버 - JSON-RPC 2.0
// made by cray
// https://itadventure.tistory.com/

$db_path = "Z:/www/db/sqlite.db";

$client_type='codeium'; // 윈드서프
function createErrorResponse($id, $code, $message, $data = null) {
    $response = [
        "jsonrpc" => "2.0",
        "id" => $id,
        "error" => [
            "code" => $code,
            "message" => $message
        ]
    ];
    
    if ($data !== null) {
        $response["error"]["data"] = $data;
    }
    
    return $response;
}

// 성공 응답 생성 함수
function createSuccessToolListResponse($id, $result) {
    return [
        "jsonrpc" => "2.0",
        "id" => $id,
        "result" => $result
    ];
}

// 성공 응답 생성 함수
function createSuccessResponse($id, $result) {
    return [
        "jsonrpc" => "2.0",
        "id" => $id,
        "result" => [
            "isError" => false,
            "content" => $result 
        ]
    ];
}

// 도구 정의 함수
function getTools() {
    // JSON-RPC 2.0 형식의 도구 정의
    return [
        "tools" => [
            [
                "name" => "listTables",
                "description" => "SQLite 테이블 목록을 조회합니다.",
                "inputSchema" => [
                    "type" => "object",
                    "properties" => (object)[],
                    "additionalProperties" => false 
                ]
            ],
            [
                "name" => "describeTable",
                "description" => "테이블의 구조를 조회합니다.",
                "inputSchema" => [
                    "type" => "object",
                    "properties" => [
                        "table" => [
                            "type" => "string",
                            "description" => "조회할 테이블 이름"
                        ]
                    ],
                    "required" => ["table"],
                    "additionalProperties" => false 
                ]
            ],
            [
                "name" => "executeQuery",
                "description" => 
                    "자연어 질문을 SQL로 작성하여 전송해 주세요.\n\n".
                    "테이블 정보:\n".
                    "- coffees: 커피 (id, name, price, origin)\n".
                    "- books: 책 (id, title, author, pages, price)\n\n".
                    "질문 예시:\n".
                    "- '해리포터 책' → books 테이블에서 title이 '해리포터'인 데이터 조회\n".
                    "- '4000원 넘는 커피' → coffees 테이블에서 price > 4000 조건으로 검색\n".
                    "결과는 사용자에게 표로 보여 주세요.".
                    "데이터는 영어 번역/음역 과정을 거치지 말고 원본을 사용하세요",
                "inputSchema" => [
                  "type" => "object",
                  "properties" => [
                    "executeQuery" => [
                      "type" => "string",
                      "description" => 
                        "질문을 SQL문으로 작성하여 전송해 주세요."
                        // ." select 만 허용."
                    ]
                  ],
                  "required" => ["executeQuery"],
                  "additionalProperties" => false 
                ]
            ]
        ]
    ];
}

// 초기화 메소드를 처리할 함수
function handleInitialize($params) {
    global $client_type;
    // 클라이언트 정보와 기능(capabilities) 저장 로직을 추가할 수 있음
    $protocolVersion = $params['protocolVersion'] ?? '2024-11-05';
    $clientInfo = $params['clientInfo'] ?? [];

    // 윈드서프
    if($clientInfo['name']=='codeium-client'){
        $client_type='codeium';
    }

    // 커서
    // {"name":"cursor-vscode","version":"1.0.0"}
    if($clientInfo['name']=='cursor-vscode'){
        $client_type='cursor';
    }

    // 클로드
    // claude-ai
    if($clientInfo['name']=='claude-ai'){
        $client_type='claude';
    }
    
    // 서버 정보와 기능 반환
    return [
        'protocolVersion' => $protocolVersion,        
        'serverInfo' => [
            'name' => 'sqlite-mcp-server',
            'version' => '1.0.0'
        ],
        'capabilities' => [
            "tools" => [
                "listChanged" => true
            ],
            "prompts" => [
                "listChanged" => false
            ],
            "resources" => [
                "listChanged" => false  
            ],
            "logging" => [
                "level" => "info"
            ]
        ]
    ];
}

// 데이터베이스 연결 함수
function connectDB() {
    global $db_path;
    try {
        $conn = new PDO("sqlite:$db_path");
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $conn;
    } catch(PDOException $e) {
        error_log("SQLite DB 연결 오류: " . $e->getMessage());
        return null;
    }
}

function formatQueryResults($results) {
    if (empty($results)) {
        return "검색 결과가 없습니다.";
    }
    
    $output = "검색 결과 (" . count($results) . "건):\n\n";
    
    foreach ($results as $index => $row) {
        $output .= "=== " . ($index + 1) . "번째 결과 ===\n";
        foreach ($row as $column => $value) {
            $output .= "$column: $value\n";
        }
        $output .= "\n";
    }
    
    return $output;
}

function executeQuery($args) {
    $query = $args['executeQuery'] ?? '';
    if (empty($query)) {
        return [
            "code" => -32602,
            "message" => "Query parameter is required",
            "data" => null
        ];
    }
    
    // 쿼리 실행
    $conn = connectDB();
    if (!$conn) {
        return [
            "code" => -32603,
            "message" => "데이터베이스 연결에 실패했습니다.",
            "data" => null
        ];
    }
        
    // 쿼리 실행
    $stmt = $conn->query($query);

    $results = [];
    if(strpos(strtoupper($query), 'SELECT') === 0) {
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $results[] = [
                "type" => "text",
                "text" => json_encode($row, JSON_UNESCAPED_UNICODE)
            ];
        }
    }
    else {
        $results[] = [
            "type" => "text",
            "text" => "Query executed successfully"
        ];
    }
    
    // 결과 포맷팅
    $formattedResults = formatQueryResults($results);
    
    return [
        [
            'type' => 'text',
            'text' => $formattedResults,
            'stmt' => $stmt
        ]
    ];        
}

// 테이블 목록 가져오기 함수
function listTables($params) {
    $conn = connectDB();
    if (!$conn) {
        return [
            "code" => -32603,
            "message" => "데이터베이스 연결에 실패했습니다.",
            "data" => null
        ];
    }
    
    try {
        // SQLite에서는 테이블 목록을 sqlite_master 테이블에서 조회
        $stmt = $conn->query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
        $tables = [];
        
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $tables[] = ["type" => "text", "text" => $row["name"]];
        }
        
        return $tables;
    } catch(PDOException $e) {
        return [
            "code" => -32603,
            "message" => $e->getMessage(),
            "data" => null
        ];
    } finally {
        $conn = null;
    }
}


// 테이블 구조 조회 함수
function describeTable($params) {
    $tableName = $params["table"] ?? "";
    
    if (empty($tableName)) {
        return [
            "code" => -32602,
            "message" => "테이블 이름이 필요합니다.",
            "data" => null
        ];
    }
    
    $conn = connectDB();
    if (!$conn) {
        return [
            "code" => -32603,
            "message" => "데이터베이스 연결에 실패했습니다.",
            "data" => null
        ];
    }
    
    try {
        // SQLite의 테이블 구조 조회
        $stmt = $conn->query("PRAGMA table_info('$tableName')");
        $columns = [];
        
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $columns[] = [
                "Field" => $row["name"],
                "Type" => $row["type"],
                "Null" => $row["notnull"] == 0 ? "YES" : "NO",
                "Key" => $row["pk"] == 1 ? "PRI" : "",
                "Default" => $row["dflt_value"],
                "Extra" => ""
            ];
        }

        $text = "Table: $tableName\nColumns:\n";
        foreach ($columns as $column) {
            $text .= "- " . $column["Field"] . ": " . $column["Type"] . ", " . $column["Null"] . ", " . $column["Key"] . ", " . $column["Default"] . ", " . $column["Extra"] . "\n";
        }
        return [[
            "type" => "text",
            "text" => $text
        ]];
        // return $columns;
    } catch(PDOException $e) {
        return [
            "code" => -32603,
            "message" => $e->getMessage(),
            "data" => null
        ];
    } finally {
        $conn = null;
    }
}

// 메인 루프
while (!feof(STDIN)) {
    $input = fgets(STDIN);
    if (!$input) {
        usleep(100000);
        continue;
    }

    $input = trim($input);
    
    $req = json_decode($input, true);

    if (!$req) {
        // JSON 파싱 오류
        $errorResponse = createErrorResponse(null, -32700, "파싱 오류: 잘못된 JSON");
        echo json_encode($errorResponse) . "\n";
        flush();
        continue;
    }
    
    // JSON-RPC 2.0 필수 필드 확인
    $id = $req["id"] ?? null;
    $method = $req["method"] ?? "";
    $params = $req["params"] ?? [];
    $jsonrpc = $req["jsonrpc"] ?? "1.0";
    
    // 메서드 이름이 없는 경우
    if (empty($method)) {
        $errorResponse = createErrorResponse($id, -32600, "잘못된 요청: 메서드 이름이 없습니다.");
        echo json_encode($errorResponse) . "\n";
        flush();
        continue;
    }
    
    // 결과 변수 초기화
    $result = null;
    $error = null;
    
    // 이미 응답을 보냈는지 확인
    $alreay_response = false;
    $response_type = 2;
    // 메서드에 따라 다른 동작 수행    
    try {
        switch ($method) {
            case "initialize":
                $result = handleInitialize($params);
                $response_type = 2;
                if($client_type == 'cursor' || $client_type == 'claude')
                {
                    $response_type = 1;
                }
                break;
            case "getTools":
            case "list_tools":
            case "tools/list":
                $result = getTools();
                $response_type = 1;
                break;
            case "tools/call":
                switch($params["name"]){
                    case "listTables":
                        $result = listTables($params["arguments"]);    
                        break;
                    case "describeTable":
                        $result = describeTable($params["arguments"]);    
                        break;
                    case "executeQuery":
                        $result = executeQuery($params["arguments"]);
                        break;
                }
                $response_type = 2;
                break;
                
            case "listTable":
            case "listTables":
                $result = listTables($params);
                // 오류 확인
                if (isset($result["code"])) {
                    $error = $result;
                    $result = null;
                }
                $response_type = 2;
                break;
                
            case "mcp0_describeTable":
            case "describeTable":
            case "describe":
                $result = describeTable($params);
                // 오류 확인
                if (isset($result["code"])) {
                    $error = $result;
                    $result = null;
                }
                $response_type = 2;
                break;
                
            case "echo":
                $result = ["context" => "Received: " . ($params["prompt"] ?? "")];
                $response_type = 2;
                break;
                                
            default:
                $error = [
                    "code" => -32601,
                    "message" => "메서드를 찾을 수 없습니다: " . $method
                ];
                break;
        }
    } catch (Exception $e) {
        $error = [
            "code" => -32603,
            "message" => "내부 오류: " . $e->getMessage()
        ];
    }
    
    if($alreay_response == false)
    {
        // 응답 생성 및 출력
        if ($error !== null) {
            $response = createErrorResponse($id, $error["code"], $error["message"], $error["data"] ?? null);
        } else if ($response_type == 1) {
            $response = createSuccessToolListResponse($id, $result);
        } else if ($response_type == 2) {
            $response = createSuccessResponse($id, $result);
        }
        
        echo json_encode($response) . "\n";
    }
    flush();
}

소스 상단의 sqlite.db 파일은 여러분의 폴더에 맞게 수정해주셔야 합니다.

$db_path = "Z:/www/db/sqlite.db";

이제 이 소스코드를 Winsurf의 MCP 서버에 연결할 겁니다.
망치 아이콘을 클릭하고 Configure 를 클릭,

'View raw config'를 선택하신 다음,

구조가 아래와 같을 텐데요.

{
  "mcpServers": {
     :
  }
}

아래와 같이 수정해 줍니다.

{
  "mcpServers": {
     :
    "sqlite_mcp": {
      "command": "php",
      "args": [
        "C:\\xampp\\htdocs\\sqlitemcp\\sqlite_mcp.php"
      ],
      "env": {}
    }
  }
}

주의하실 점은 MCP서버를 2개 이상 사용할 경우,
콤마를 꼭 넣어주셔야 합니다.

다음으로 Windsurf 에게 MCP 서버를 인식시켜주면 되는데요.
금방 펼쳤던 MCP 도구 아래 'Refresh'를 클릭하시면 됩니다.

그리고 나서 스크롤바를 약간 내리면 MCP 도구 목록이 보일텐데요. sqlite_mcp 3 tools 가 뜨면 제대로 연결된 것입니다.

이제 이 SQLITE 를 테스트해보겠습니다.
먼저 아래 프롬프트를 입력해 주세요.

sqlite 테이블 목록 보여줘

지난 번에 회원가입/로그인 기능을 만들었기에 users 테이블이 보이는군요.

SQLite 데이터베이스에 현재 다음과 같은 테이블이 있습니다:

users
데이터베이스에는 현재 이 하나의 테이블만 존재합니다. 
이 테이블의 구조를 확인하거나 데이터를 조회하시겠습니까?

다음 프롬프트입니다.

users 테이블 구조 보여줘

뭐 이런건 간단한거니 다음으로 넘어가겠습니다.

사용자 테이블에 임의로 영문 이름, 닉네임, 자기소개를 지어 10명을 추가해줘,

'사용자'라고 말해도 AI가 찰떡같이 users로 알아듣습니다.
그리고 다 되었다고 하네요.

users 테이블에 임의의 영문 이름, 닉네임, 자기소개를 가진 10명의 사용자가 
성공적으로 추가되었습니다.

추가된 데이터 조회나 다른 작업이 필요하시면 말씀해 주세요!

정말인가 볼까요?

사용자 목록 보여줘

와우~

똑똑하게도 패스워드는 알아서 목록에 포함하지 않았네요.
이런식으로 자연어로 sqlite 데이터베이스를 지지고 볶고 할 수 있는게 sqlite MCP입니다.

아래와 같이 대강 지시해도,

주간식단 테이블 만들고 대충 데이터를 넣어줘.

거의 다 알아서 해주기도 합니다.

주간식단(weekly_menu) 테이블을 다시 생성하고, 임의의 데이터를 6건(월~화, 아침/점심/저녁) 입력 완료했습니다.

예시 데이터는 다음과 같습니다:

2025-07-07 월요일 아침: 김치볶음밥 (매콤함)
2025-07-07 월요일 점심: 된장찌개
2025-07-07 월요일 저녁: 치킨샐러드 (닭가슴살)
2025-07-08 화요일 아침: 토스트
2025-07-08 화요일 점심: 비빔밥 (계란 추가)
2025-07-08 화요일 저녁: 스파게티
식단을 보여줘

주의하실 점은, 너무 기능이 파워풀해서 MCP 기능은 내 PC xampp 에서만 사용하셔야 한다는 점입니다.
인터넷 홈페이지에 올리시면 해킹당할 수 있어요! 요주의!

다만, 이 MCP 서버에 AI 기능이 있는 것은 아닙니다. ( 넣을수도 있지만 api 과금이.. )
AI 는 윈드서프가 담당하는 것이니 해당 부분 혼동 없으시기 바랍니다.

MCP로 제어하는 sqlite, 흥미로운 주제가 되셨을까요?
아무쪼록 방문하시는 모든 분들께 감사! 드립니다.

혹여라도 Windsurf AI 코딩 도구를 구독하실분은 아래 링크를 통해 구독하시면 저와 독자님께 250 크레딧이 추가로 충전됩니다! 후원하실 분은 참고해 주세요~

https://windsurf.com/refer?referral_code=1apci34g8vgz0qom