728x90

Oracle DB Context MCP Server를 사용하여 특정 오라클 데이터베이스 테이블의 스키마 정보를 가져오고, 이를 기반으로 Vuetify를 활용해 CRUD(Create, Read, Update, Delete) 화면을 동적으로 생성하는 예제.
아래에서는 Oracle DB Context MCP Server를 통해 테이블 스키마를 조회하고, Vue.js와 Vuetify를 사용하여 해당 테이블에 대한 CRUD 인터페이스를 생성하는 예제를 단계별로 설명합니다.
전제 조건
-
Oracle DB Context MCP Server:
-
설치 및 설정 완료 (.env 파일에 오라클 접속 정보 포함).
-
실행 명령: uv run main.py
-
MCP 서버가 테이블 스키마 조회 및 데이터 질의를 지원한다고 가정.
-
-
필요 패키지:
-
Backend: mcp[cli]>=1.4.0, python-oracledb>=1.2.0, python-dotenv>=1.0.0
-
Frontend: vue, vuetify, axios
-
-
프로젝트 구조:
project/ ├── backend/ │ ├── main.py # MCP 서버 │ ├── .env # 오라클 접속 정보 ├── frontend/ │ ├── src/ │ │ ├── components/ │ │ ├── App.vue │ │ ├── main.js │ ├── package.json
-
테이블 예시:
-
테이블 이름: EMPLOYEES
-
컬럼:
EMPLOYEE_ID (NUMBER),
FIRST_NAME (VARCHAR2),
LAST_NAME (VARCHAR2),
HIRE_DATE (DATE)
-
전체 흐름
-
MCP 서버:
-
get_table_schema 도구로 EMPLOYEES 테이블의 스키마를 조회.
-
query_table, insert_record, update_record, delete_record 도구로 CRUD 작업 지원.
-
-
Vue.js/Vuetify 프론트엔드:
-
MCP 서버와 통신하여 스키마와 데이터를 가져옴.
-
Vuetify의 v-data-table과 v-dialog를 사용하여 CRUD 인터페이스 구성.
-
-
통신:
-
Axios를 통해 MCP 서버의 JSON-RPC 엔드포인트와 통신.
-
1. Backend: Oracle DB Context MCP Server
MCP 서버는 테이블 스키마 조회와 CRUD 작업을 지원하는 도구를 제공합니다. 아래는 관련 도구를 포함한 main.py 예제입니다.
import asyncio
import oracledb
import mcp.server
import mcp.types as types
from mcp.server.stdio import stdio_server
from dotenv import load_dotenv
import os
# 환경 변수 로드
load_dotenv()
# 오라클 접속 정보
ORACLE_CONFIG = {
"user": os.getenv("ORACLE_USER"),
"password": os.getenv("ORACLE_PASSWORD"),
"dsn": os.getenv("ORACLE_DSN")
}
# MCP 서버 설정
app = mcp.server.Server("oracle-db-context")
# 테이블 스키마 조회
@app.tool("get_table_schema")
async def get_table_schema(table_name: str) -> types.Resource:
try:
conn = oracledb.connect(**ORACLE_CONFIG)
cursor = conn.cursor()
cursor.execute("""
SELECT column_name, data_type, nullable
FROM user_tab_columns
WHERE table_name = UPPER(:1)
""", [table_name])
schema = [{"column": row[0], "type": row[1], "nullable": row[2]} for row in cursor.fetchall()]
cursor.close()
conn.close()
return types.Resource(
uri=f"oracle://schema/{table_name}",
name=f"Schema for {table_name}",
content=schema
)
except oracledb.DatabaseError as e:
raise Exception(f"Database error: {e}")
# 데이터 조회
@app.tool("query_table")
async def query_table(table_name: str, limit: int = 10) -> types.Resource:
try:
conn = oracledb.connect(**ORACLE_CONFIG)
cursor = conn.cursor()
query = f"SELECT * FROM {table_name} WHERE ROWNUM <= :1"
cursor.execute(query, [limit])
columns = [desc[0] for desc in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
cursor.close()
conn.close()
return types.Resource(
uri=f"oracle://data/{table_name}",
name=f"Data from {table_name}",
content=rows
)
except oracledb.DatabaseError as e:
raise Exception(f"Database error: {e}")
# 데이터 삽입
@app.tool("insert_record")
async def insert_record(table_name: str, record: dict) -> types.Resource:
try:
conn = oracledb.connect(**ORACLE_CONFIG)
cursor = conn.cursor()
columns = list(record.keys())
placeholders = ", ".join([":%d" % (i + 1) for i in range(len(columns))])
query = f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({placeholders})"
cursor.execute(query, list(record.values()))
conn.commit()
cursor.close()
conn.close()
return types.Resource(
uri=f"oracle://data/{table_name}",
name=f"Inserted record in {table_name}",
content={"status": "success"}
)
except oracledb.DatabaseError as e:
raise Exception(f"Database error: {e}")
# 데이터 수정
@app.tool("update_record")
async def update_record(table_name: str, record: dict, key_column: str) -> types.Resource:
try:
conn = oracledb.connect(**ORACLE_CONFIG)
cursor = conn.cursor()
columns = list(record.keys())
set_clause = ", ".join([f"{col} = :{i + 1}" for i, col in enumerate(columns)])
query = f"UPDATE {table_name} SET {set_clause} WHERE {key_column} = :{len(columns) + 1}"
values = list(record.values()) + [record[key_column]]
cursor.execute(query, values)
conn.commit()
cursor.close()
conn.close()
return types.Resource(
uri=f"oracle://data/{table_name}",
name=f"Updated record in {table_name}",
content={"status": "success"}
)
except oracledb.DatabaseError as e:
raise Exception(f"Database error: {e}")
# 데이터 삭제
@app.tool("delete_record")
async def delete_record(table_name: str, key_column: str, key_value: str) -> types.Resource:
try:
conn = oracledb.connect(**ORACLE_CONFIG)
cursor = conn.cursor()
query = f"DELETE FROM {table_name} WHERE {key_column} = :1"
cursor.execute(query, [key_value])
conn.commit()
cursor.close()
conn.close()
return types.Resource(
uri=f"oracle://data/{table_name}",
name=f"Deleted record in {table_name}",
content={"status": "success"}
)
except oracledb.DatabaseError as e:
raise Exception(f"Database error: {e}")
# 서버 실행
async def main():
async with stdio_server() as streams:
await app.run(
streams[0], streams[1],
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
설정
-
.env 파일:
ORACLE_USER=scott ORACLE_PASSWORD=tiger ORACLE_DSN=localhost:1521/orcl
-
실행:
uv run main.py
2. Frontend: Vue.js + Vuetify CRUD 애플리케이션
Vue.js 프로젝트를 생성하고 Vuetify를 통합하여 EMPLOYEES 테이블에 대한 CRUD 인터페이스를 구현합니다.
프로젝트 설정
-
Vue 프로젝트 생성:
vue create vuetify-crud cd vuetify-crud vue add vuetify npm install axios
-
Vuetify 설정 확인:
-
src/plugins/vuetify.js에 Vuetify가 올바르게 설정되었는지 확인.
-
주요 컴포넌트: EmployeeCrud.vue
EMPLOYEES 테이블의 CRUD 인터페이스를 구현하는 컴포넌트입니다.
<template>
<v-container>
<h2>Employees CRUD</h2>
<!-- 데이터 테이블 -->
<v-data-table
:headers="headers"
:items="employees"
:items-per-page="10"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>Employees</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn color="primary" @click="openCreateDialog">Add Employee</v-btn>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" @click="openEditDialog(item)">mdi-pencil</v-icon>
<v-icon small @click="deleteEmployee(item)">mdi-delete</v-icon>
</template>
</v-data-table>
<!-- CRUD 다이얼로그 -->
<v-dialog v-model="dialog" max-width="500px">
<v-card>
<v-card-title>
<span>{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-form v-model="valid">
<v-text-field
v-for="field in schema"
:key="field.column"
v-model="editedItem[field.column]"
:label="field.column"
:rules="[v => !!v || `${field.column} is required`]"
:type="field.type.includes('NUMBER') ? 'number' : 'text'"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDialog">Cancel</v-btn>
<v-btn color="blue darken-1" text @click="saveEmployee" :disabled="!valid">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
tableName: 'EMPLOYEES',
schema: [],
headers: [],
employees: [],
dialog: false,
valid: false,
editedIndex: -1,
editedItem: {},
defaultItem: {},
};
},
computed: {
formTitle() {
return this.editedIndex === -1 ? 'New Employee' : 'Edit Employee';
},
},
async created() {
await this.fetchSchema();
await this.fetchEmployees();
},
methods: {
// MCP 서버와 통신
async callMcpTool(tool, params) {
try {
const response = await axios.post('http://localhost:8000/jsonrpc', {
jsonrpc: '2.0',
method: tool,
params,
id: 1,
});
return response.data.result.content;
} catch (error) {
console.error(`Error calling ${tool}:`, error);
throw error;
}
},
// 스키마 조회
async fetchSchema() {
this.schema = await this.callMcpTool('get_table_schema', { table_name: this.tableName });
this.headers = this.schema.map(field => ({
text: field.column,
value: field.column,
})).concat([{ text: 'Actions', value: 'actions', sortable: false }]);
this.defaultItem = Object.fromEntries(this.schema.map(field => [field.column, '']));
},
// 데이터 조회
async fetchEmployees() {
this.employees = await this.callMcpTool('query_table', { table_name: this.tableName, limit: 100 });
},
// 다이얼로그 열기 (생성)
openCreateDialog() {
this.editedItem = { ...this.defaultItem };
this.editedIndex = -1;
this.dialog = true;
},
// 다이얼로그 열기 (수정)
openEditDialog(item) {
this.editedItem = { ...item };
this.editedIndex = this.employees.indexOf(item);
this.dialog = true;
},
// 다이얼로그 닫기
closeDialog() {
this.dialog = false;
this.editedItem = { ...this.defaultItem };
this.editedIndex = -1;
},
// 저장 (생성/수정)
async saveEmployee() {
if (this.editedIndex === -1) {
// 생성
await this.callMcpTool('insert_record', { table_name: this.tableName, record: this.editedItem });
} else {
// 수정
await this.callMcpTool('update_record', {
table_name: this.tableName,
record: this.editedItem,
key_column: 'EMPLOYEE_ID',
});
}
await this.fetchEmployees();
this.closeDialog();
},
// 삭제
async deleteEmployee(item) {
await this.callMcpTool('delete_record', {
table_name: this.tableName,
key_column: 'EMPLOYEE_ID',
key_value: item.EMPLOYEE_ID,
});
await this.fetchEmployees();
},
},
};
</script>
main.js 설정
Vuetify와 Vue를 초기화합니다.
import Vue from 'vue';
import App from './App.vue';
import vuetify from './plugins/vuetify';
Vue.config.productionTip = false;
new Vue({
vuetify,
render: h => h(App),
}).$mount('#app');
App.vue
EmployeeCrud 컴포넌트를 렌더링합니다.
<template>
<v-app>
<v-main>
<EmployeeCrud />
</v-main>
</v-app>
</template>
<script>
import EmployeeCrud from './components/EmployeeCrud.vue';
export default {
name: 'App',
components: {
EmployeeCrud,
},
};
</script>
3. 실행 방법
-
Backend 실행:
cd backend uv run main.py
-
MCP 서버가 http://localhost:8000/jsonrpc에서 실행된다고 가정.
-
-
Frontend 실행:
cd frontend npm run serve
-
Vue 애플리케이션이 http://localhost:8080에서 실행.
-
-
브라우저 접속:
-
http://localhost:8080에 접속하여 EMPLOYEES 테이블의 CRUD 인터페이스를 확인.
-
코드 설명
-
Backend (MCP 서버):
-
get_table_schema: EMPLOYEES 테이블의 컬럼 이름, 데이터 타입, null 여부를 반환.
-
query_table: 테이블 데이터를 최대 limit 행까지 조회.
-
insert_record, update_record, delete_record: 각각 레코드 삽입, 수정, 삭제를 처리.
-
-
Frontend (Vue/Vuetify):
-
fetchSchema: MCP 서버에서 스키마를 가져와 v-data-table의 헤더와 폼 필드를 동적으로 생성.
-
fetchEmployees: 테이블 데이터를 가져와 v-data-table에 표시.
-
v-data-table: 데이터 표시 및 액션 버튼(수정, 삭제) 제공.
-
v-dialog: 생성/수정 폼을 동적으로 렌더링.
-
Axios를 통해 MCP 서버의 JSON-RPC 엔드포인트와 통신.
-
-
Vuetify 컴포넌트:
-
v-data-table은 데이터를 테이블 형식으로 표시하며, 액션 버튼을 통해 수정/삭제 가능.
-
v-dialog는 스키마 기반으로 입력 폼을 생성하여 사용자 입력을 받음.
-
추가 고려 사항
-
스키마 동적 처리:
-
schema를 기반으로 폼 필드와 테이블 헤더를 동적으로 생성하므로, 다른 테이블(예: DEPARTMENTS)로 변경하려면 tableName만 수정하면 됩니다.
-
-
데이터 타입 처리:
-
현재는 NUMBER와 기타 타입을 단순히 number와 text로 매핑. DATE 등 특수 타입은 v-date-picker 같은 Vuetify 컴포넌트를 추가로 사용 가능.
-
-
보안:
-
.env 파일로 오라클 접속 정보를 관리.
-
MCP 서버에 인증(OAuth 2.1 등)을 추가하여 보안 강화.
-
-
성능:
-
대량 데이터의 경우, query_table에 페이징(OFFSET/FETCH)을 추가.
-
MCP 서버의 스키마 캐싱을 활용하여 반복 조회 부담 감소.
-
-
에러 처리:
-
Axios 호출에 에러 핸들링을 강화하고, 사용자에게 알림(Snackbar 등) 표시.
-
참고 자료
추가 질문
-
특정 컬럼(예: HIRE_DATE)에 대해 v-date-picker 같은 특수 입력 필드가 필요하시면 예제를 추가로 제공할 수 있습니다.
-
다른 테이블이나 복잡한 스키마(외래 키 등)를 다루는 예제가 필요하시면 말씀해주세요.
728x90
'Python을 배워보자' 카테고리의 다른 글
MCP 구축과 활용: 커서와 파이썬을 활용한 상세 가이드 (0) | 2025.04.20 |
---|---|
Python 패키지 관리자 uv: 속도와 편리함의 새로운 표준 (0) | 2025.04.20 |
Python으로 오라클 필드명과 카멜 케이스 변수 매핑 자동화하기 (0) | 2025.04.19 |
ㅎㅎ (1) | 2025.03.06 |
오라클에서 테이블의 SELECT 결과를 그대로 INSERT문으로 변환 (0) | 2025.03.03 |