Recent Posts
Recent Comments
반응형
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

오늘도 공부

Supabase Custom MCP Server 본문

AI/Cursor

Supabase Custom MCP Server

행복한 수지아빠 2025. 7. 21. 14:37
반응형

커스텀으로 작성된 MCP Server

(쓰기,수정까지 가능한 버전임) -> 개발버전에서만...

// .env
SUPABASE_URL=https://xo...
SUPABASE_ANON_KEY=....
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createClient } from '@supabase/supabase-js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import dotenv from 'dotenv';

// .env 파일 로드
dotenv.config();

// Supabase 클라이언트 초기화
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseKey) {
  console.error("Error: SUPABASE_URL and SUPABASE_ANON_KEY environment variables are required");
  process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);

// MCP 서버 생성
const server = new Server(
  {
    name: "supabase-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 사용 가능한 도구들 정의
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "query",
        description: "Execute a SELECT query on Supabase",
        inputSchema: {
          type: "object",
          properties: {
            table: { type: "string", description: "Table name" },
            columns: { type: "string", description: "Columns to select (default: *)" },
            filters: { type: "object", description: "Filter conditions" },
            limit: { type: "number", description: "Limit results" }
          },
          required: ["table"],
        },
      },
      {
        name: "insert",
        description: "Insert data into Supabase table",
        inputSchema: {
          type: "object",
          properties: {
            table: { type: "string", description: "Table name" },
            data: { type: "object", description: "Data to insert" }
          },
          required: ["table", "data"],
        },
      },
      {
        name: "update",
        description: "Update data in Supabase table",
        inputSchema: {
          type: "object",
          properties: {
            table: { type: "string", description: "Table name" },
            data: { type: "object", description: "Data to update" },
            filters: { type: "object", description: "Filter conditions" }
          },
          required: ["table", "data", "filters"],
        },
      },
      {
        name: "delete",
        description: "Delete data from Supabase table",
        inputSchema: {
          type: "object",
          properties: {
            table: { type: "string", description: "Table name" },
            filters: { type: "object", description: "Filter conditions" }
          },
          required: ["table", "filters"],
        },
      },
      {
        name: "create_table",
        description: "Create a new table using Supabase SQL",
        inputSchema: {
          type: "object",
          properties: {
            sql: { type: "string", description: "CREATE TABLE SQL statement" }
          },
          required: ["sql"],
        },
      },
      {
        name: "list_tables",
        description: "List all tables in the database",
        inputSchema: {
          type: "object",
          properties: {},
        },
      },
    ],
  };
});

// 도구 실행 핸들러
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "query": {
        let query = supabase.from(args.table).select(args.columns || '*');
        
        // 필터 적용
        if (args.filters) {
          Object.entries(args.filters).forEach(([key, value]) => {
            query = query.eq(key, value);
          });
        }
        
        if (args.limit) {
          query = query.limit(args.limit);
        }
        
        const { data, error } = await query;
        
        if (error) throw error;
        
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(data, null, 2),
            },
          ],
        };
      }

      case "insert": {
        const { data, error } = await supabase
          .from(args.table)
          .insert(args.data)
          .select();
        
        if (error) throw error;
        
        return {
          content: [
            {
              type: "text",
              text: `Successfully inserted: ${JSON.stringify(data, null, 2)}`,
            },
          ],
        };
      }

      case "update": {
        let query = supabase.from(args.table).update(args.data);
        
        // 필터 적용
        Object.entries(args.filters).forEach(([key, value]) => {
          query = query.eq(key, value);
        });
        
        const { data, error } = await query.select();
        
        if (error) throw error;
        
        return {
          content: [
            {
              type: "text",
              text: `Successfully updated: ${JSON.stringify(data, null, 2)}`,
            },
          ],
        };
      }

      case "delete": {
        let query = supabase.from(args.table).delete();
        
        // 필터 적용
        Object.entries(args.filters).forEach(([key, value]) => {
          query = query.eq(key, value);
        });
        
        const { data, error } = await query.select();
        
        if (error) throw error;
        
        return {
          content: [
            {
              type: "text",
              text: `Successfully deleted records`,
            },
          ],
        };
      }

      case "create_table": {
        // Service role key가 필요합니다
        const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
        if (!serviceRoleKey) {
          throw new Error("Service role key required for DDL operations");
        }
        
        const adminSupabase = createClient(supabaseUrl, serviceRoleKey);
        const { data, error } = await adminSupabase.rpc('exec_sql', {
          sql: args.sql
        });
        
        if (error) throw error;
        
        return {
          content: [
            {
              type: "text",
              text: "Table created successfully",
            },
          ],
        };
      }

      case "list_tables": {
        const { data, error } = await supabase
          .from('information_schema.tables')
          .select('table_name')
          .eq('table_schema', 'public');
        
        if (error) throw error;
        
        return {
          content: [
            {
              type: "text",
              text: `Tables: ${data.map(t => t.table_name).join(', ')}`,
            },
          ],
        };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Error: ${error.message}`,
        },
      ],
    };
  }
});

// 서버 시작
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Supabase MCP server started");
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

클로드 코드에 추가시

claude mcp add-json supabase-custom '{"command":"node","args":["/path/to/mcp-custom-supabase.js"],"type":"stdio"}'

그리고 Claude code에서 supabase 세팅후 디비 세팅 요청하면 됨.

테스트 코드 실행

이제 만든 MCP를 테스트 해보자

  1. 테이블 생성: Supabase는 API를 통한 직접적인 DDL(CREATE TABLE) 실행을 지원하지
   않습니다
  2. 테이블 목록 조회: information_schema 접근이 제한되어 있습니다

  3. 테이블 생성은 Supabase Dashboard에서:
    - setup-table.md 파일을 참고하세요
    - SQL Editor에서 테이블 생성 SQL 실행
  4. 먼저 테이블을 생성하세요:
  
  CREATE TABLE IF NOT EXISTS test_mcp_users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    age INTEGER,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW())
  );
  
  5. 테이블 생성 후 테스트 실행:
  npm test
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";

// 테스트용 테이블 이름
const TEST_TABLE = "test_mcp_users";

// MCP 클라이언트 생성 및 연결
async function createClient() {
  const transport = new StdioClientTransport({
    command: "node",
    args: ["./main.js"],
    env: {
      ...process.env,
      SUPABASE_URL: process.env.SUPABASE_URL,
      SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY
    }
  });

  const client = new Client({
    name: "test-client",
    version: "1.0.0"
  }, {
    capabilities: {}
  });

  await client.connect(transport);
  return client;
}

// 도구 실행 헬퍼 함수
async function callTool(client, toolName, args) {
  try {
    const result = await client.callTool({ name: toolName, arguments: args });
    console.log(`\n✅ ${toolName} 성공:`);
    console.log(result.content[0].text);
    return result;
  } catch (error) {
    console.error(`\n❌ ${toolName} 실패:`, error.message);
    throw error;
  }
}

// 메인 테스트 함수
async function runTests() {
  console.log("🧪 Supabase MCP 서버 테스트 시작...\n");
  
  let client;
  try {
    // 클라이언트 연결
    client = await createClient();
    console.log("✅ MCP 서버 연결 성공\n");

    // 0. 테이블 생성 (Service Role Key 필요)
    if (process.env.SUPABASE_SERVICE_ROLE_KEY) {
      console.log("0️⃣ 테이블 생성 테스트");
      const createTableSQL = `
        CREATE TABLE IF NOT EXISTS test_mcp_users (
          id SERIAL PRIMARY KEY,
          name TEXT NOT NULL,
          email TEXT UNIQUE NOT NULL,
          age INTEGER,
          created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW())
        )
      `;
      
      try {
        await callTool(client, "create_table", {
          sql: createTableSQL
        });
        
        // RLS 비활성화
        await callTool(client, "create_table", {
          sql: "ALTER TABLE test_mcp_users DISABLE ROW LEVEL SECURITY"
        });
      } catch (error) {
        console.log("테이블이 이미 존재하거나 생성 실패 (계속 진행)");
      }
    } else {
      console.log("⚠️  Service Role Key가 없어 테이블 생성 건너뜀");
    }

    // 1. 테이블 목록 조회
    console.log("\n1️⃣ 테이블 목록 조회 테스트");
    await callTool(client, "list_tables", {});

    // 2. 데이터 삽입 테스트
    console.log("\n2️⃣ 데이터 삽입 테스트");
    const insertData = {
      name: "테스트 사용자",
      email: "test@example.com",
      age: 25
    };
    await callTool(client, "insert", {
      table: TEST_TABLE,
      data: insertData
    });

    // 3. 데이터 조회 테스트
    console.log("\n3️⃣ 데이터 조회 테스트");
    await callTool(client, "query", {
      table: TEST_TABLE,
      columns: "*",
      limit: 10
    });

    // 4. 데이터 수정 테스트
    console.log("\n4️⃣ 데이터 수정 테스트");
    await callTool(client, "update", {
      table: TEST_TABLE,
      data: { name: "수정된 사용자" },
      filters: { email: "test@example.com" }
    });

    // 5. 필터링된 조회 테스트
    console.log("\n5️⃣ 필터링된 조회 테스트");
    await callTool(client, "query", {
      table: TEST_TABLE,
      columns: "name, email",
      filters: { email: "test@example.com" }
    });

    // 6. 데이터 삭제 테스트
    console.log("\n6️⃣ 데이터 삭제 테스트");
    await callTool(client, "delete", {
      table: TEST_TABLE,
      filters: { email: "test@example.com" }
    });

    // 7. 삭제 확인
    console.log("\n7️⃣ 삭제 확인 조회");
    await callTool(client, "query", {
      table: TEST_TABLE,
      filters: { email: "test@example.com" }
    });

    console.log("\n\n✅ 모든 테스트 완료!");

  } catch (error) {
    console.error("\n❌ 테스트 중 오류 발생:", error);
  } finally {
    // 클라이언트 연결 종료
    if (client) {
      await client.close();
      console.log("\n👋 MCP 서버 연결 종료");
    }
  }
}

// 환경변수 확인 및 테스트 실행
import dotenv from 'dotenv';
dotenv.config();

if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
  console.error("❌ 오류: SUPABASE_URL과 SUPABASE_ANON_KEY 환경변수가 필요합니다.");
  console.log("\n.env 파일에 다음 내용을 추가하세요:");
  console.log("SUPABASE_URL=your_supabase_url");
  console.log("SUPABASE_ANON_KEY=your_anon_key");
  console.log("SUPABASE_SERVICE_ROLE_KEY=your_service_role_key (선택사항 - 테이블 생성용)");
  process.exit(1);
}

// 테스트 실행
runTests().catch(console.error);
✅ MCP 서버 연결 성공

⚠️  Service Role Key가 없어 테이블 생성 건너뜀

1️⃣ 테이블 목록 조회 테스트

✅ list_tables 성공:
Error: relation "public.information_schema.tables" does not exist

2️⃣ 데이터 삽입 테스트

✅ insert 성공:
Error: undefined

3️⃣ 데이터 조회 테스트

✅ query 성공:
Error: relation "public.test_mcp_users" does not exist

4️⃣ 데이터 수정 테스트

✅ update 성공:
Error: undefined

5️⃣ 필터링된 조회 테스트

✅ query 성공:
Error: relation "public.test_mcp_users" does not exist

6️⃣ 데이터 삭제 테스트

✅ delete 성공:
Error: relation "public.test_mcp_users" does not exist

7️⃣ 삭제 확인 조회

✅ query 성공:
Error: relation "public.test_mcp_users" does not exist


✅ 모든 테스트 완료!

👋 MCP 서버 연결 종료
반응형

'AI > Cursor' 카테고리의 다른 글

커서 업데이트 내용 (2025.04.03)  (2) 2025.04.03
Cursor (AI IDE)의 작동 방식 이해하기  (0) 2025.03.18
Flutter Cursor Rule  (0) 2025.02.19