Notice
Recent Posts
Recent Comments
반응형
오늘도 공부
Supabase Custom MCP Server 본문
반응형
커스텀으로 작성된 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 |