mcp.ts•25.4 kB
import { Handler } from '@netlify/functions';
import { normalizeQuery, calculateSimilarity, fetchHRFCOData, Station, SearchResult } from './utils';
import { stationMapper } from './station-mapper';
import { getWaterDataByName, PipelineResult } from './water-data-pipeline';
import { smartStationMapper, SearchResult as SmartSearchResult } from './smart-station-mapper';
// StationMapper 초기화 상태
let isMapperInitialized = false;
// MCP Tools 정의 (get_water_info를 최상단으로 이동)
const tools = [
{
name: "get_water_info",
description: "관측소 검색 및 실시간 수위 데이터 통합 조회 (ChatGPT 무한 반복 방지용) - 권장 도구",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "검색어 (관측소명, 하천명, 위치)"
}
},
required: ["query"]
}
},
{
name: "search_water_station_by_name",
description: "지역명이나 강 이름으로 관측소 검색 (실제 코드 포함)",
inputSchema: {
type: "object",
properties: {
location_name: {
type: "string",
description: "검색할 지역명 또는 강 이름 (예: '한강', '서울', '부산')"
},
data_type: {
type: "string",
description: "데이터 타입",
enum: ["waterlevel", "rainfall", "dam"],
default: "waterlevel"
},
limit: {
type: "number",
description: "반환할 최대 결과 수",
default: 5,
maximum: 10
}
},
required: ["location_name"]
}
},
{
name: "get_water_info_by_location",
description: "자연어 수문 정보 조회 (실제 코드 포함)",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "자연어 쿼리 (예: '한강 수위', '서울 강우량')"
},
limit: {
type: "number",
description: "반환할 최대 결과 수",
default: 5,
maximum: 10
}
},
required: ["query"]
}
},
{
name: "recommend_nearby_stations",
description: "주변 관측소 추천 (실제 코드 포함)",
inputSchema: {
type: "object",
properties: {
location: {
type: "string",
description: "기준 위치 (지역명)"
},
radius: {
type: "number",
description: "검색 반경 (km)",
default: 20
},
priority: {
type: "string",
description: "우선순위 기준",
enum: ["distance", "relevance"],
default: "distance"
}
},
required: ["location"]
}
}
];
// MCP 메서드 핸들러들
async function mcpInitialize() {
return {
jsonrpc: "2.0",
result: {
protocolVersion: "2024-11-05",
capabilities: {
tools: {}
},
serverInfo: {
name: "K-Water 수문정보 MCP 서버",
version: "1.0.0"
}
}
};
}
async function mcpToolsList() {
return {
jsonrpc: "2.0",
result: {
tools
}
};
}
async function mcpToolsCall(toolName: string, args: any) {
try {
let result;
switch (toolName) {
case "search_water_station_by_name":
result = await searchWaterStationByName(args);
break;
case "get_water_info_by_location":
result = await getWaterInfoByLocation(args);
break;
case "get_water_info":
result = await getWaterInfoIntegrated(args);
break;
case "recommend_nearby_stations":
result = await recommendNearbyStations(args);
break;
default:
throw new Error(`Unknown tool: ${toolName}`);
}
// 통합 검색인 경우 특별한 형태로 반환
if (toolName === 'get_water_info' && (result as any).status) {
return {
jsonrpc: "2.0",
result: {
content: [
{
type: "text",
text: formatIntegratedResponse(result)
}
]
}
};
}
return {
jsonrpc: "2.0",
result: {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
}
};
} catch (error: any) {
return {
jsonrpc: "2.0",
error: {
code: -32603,
message: error.message || 'Internal error'
}
};
}
}
function mcpError(id: any, message: string) {
return {
jsonrpc: "2.0",
id,
error: {
code: -32601,
message
}
};
}
// 기존 함수 로직들 (그대로 유지)
async function searchWaterStationByName(params: any) {
const locationName = params.location_name;
const dataType = params.data_type || 'waterlevel';
const limit = Math.min(parseInt(params.limit || '5'), 10);
if (!locationName) {
throw new Error('location_name parameter required');
}
try {
// SmartStationMapper를 사용하여 지능형 검색
const searchResults = smartStationMapper.searchByName(locationName, dataType as 'dam' | 'waterlevel' | 'rainfall');
// 결과를 기존 형식으로 변환
const stations = searchResults.slice(0, limit).map(result => ({
code: result.code,
name: result.name,
address: result.region,
agency: result.agency,
real_code: result.code // 실제 HRFCO 코드
}));
const result: SearchResult = {
query: locationName,
data_type: dataType,
found_stations: stations.length,
total_available: smartStationMapper.getStats().total,
stations: stations
};
return result;
} catch (error: any) {
return {
query: locationName,
data_type: dataType,
error: `검색 중 오류 발생: ${error.message}`,
found_stations: 0,
total_available: 0,
stations: [],
note: "SmartStationMapper 오류로 인해 검색에 실패했습니다."
};
}
}
async function getWaterInfoByLocation(params: any) {
const query = params.query;
const limit = Math.min(parseInt(params.limit || '5'), 10);
if (!query) {
throw new Error('query parameter required');
}
const queryInfo = normalizeQuery(query);
try {
const data = await fetchHRFCOData(`${queryInfo.dataType}/info.json`);
const stations: Station[] = data.content || [];
if (stations.length === 0) {
return {
status: 'no_match',
message: `'${query}'에 대한 관측소를 찾을 수 없습니다`,
suggestions: []
};
}
const scoredStations = stations
.filter(station => station && station.obsnm) // null 체크 추가
.map(station => ({
station,
score: calculateSimilarity(station, queryInfo)
}))
.filter(item => item.score > 0.1)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
if (scoredStations.length === 0) {
const suggestions = stations
.filter(s => s && s.obsnm && s.addr) // null 체크 추가
.slice(0, 5)
.map(s => `${s.obsnm} (${s.addr})`)
.filter(s => s.includes(query.slice(0, 1)));
return {
status: 'no_match',
message: `'${query}'에 대한 관측소를 찾을 수 없습니다`,
suggestions: suggestions.slice(0, 3)
};
}
return {
status: 'success',
summary: `${query} 관련 ${scoredStations.length}개 관측소 발견`,
data: {
query,
data_type: queryInfo.dataType,
found_stations: scoredStations.length,
total_available: stations.length,
stations: scoredStations.map(({ station }) => {
// 실제 코드 매핑을 통해 코드 개선
const stationName = station.obsnm || '';
const mappedCode = findStationCode(stationName) || station.wlobscd || station.rfobscd || station.damcd || '';
return {
code: mappedCode,
name: stationName,
address: station.addr || '',
agency: station.agcnm || '',
real_code: mappedCode !== '' ? '실제 HRFCO 코드' : 'API 코드'
};
})
}
};
} catch (error: any) {
// API 오류 시 대체 응답
return {
status: 'error',
message: `${query} 조회 중 오류가 발생했습니다: ${error.message}`,
suggestion: 'API 키를 확인하거나 다른 검색어를 시도해보세요.'
};
}
}
async function recommendNearbyStations(params: any) {
const location = params.location;
const radius = parseInt(params.radius || '20');
const priority = params.priority || 'distance';
if (!location) {
throw new Error('location parameter required');
}
const queryInfo = normalizeQuery(location);
const data = await fetchHRFCOData('waterlevel/info.json');
const stations: Station[] = data.content || [];
const scoredStations = stations
.map(station => ({
station,
score: calculateSimilarity(station, queryInfo)
}))
.filter(item => item.score > 0.05)
.sort((a, b) => b.score - a.score)
.slice(0, 10);
const recommendations = scoredStations
.slice(0, 5)
.map(({ station }) => {
// 실제 코드 매핑을 통해 코드 개선
const stationName = station.obsnm || '';
const mappedCode = findStationCode(stationName) || station.wlobscd || '';
return {
code: mappedCode,
name: stationName,
address: station.addr || '',
agency: station.agcnm || '',
real_code: mappedCode !== '' ? '실제 HRFCO 코드' : 'API 코드'
};
});
return {
location,
radius_km: radius,
priority,
recommendations
};
}
// 통합 검색 및 데이터 조회 (ChatGPT 무한 반복 방지용)
async function getWaterInfoIntegrated(params: any) {
const query = params.query;
if (!query) {
throw new Error('query parameter required');
}
try {
// 새로운 파이프라인으로 데이터 조회
const result: PipelineResult = await getWaterDataByName(query);
if (result.found_stations === 0) {
return createErrorResponse(`'${query}' 관측소를 찾을 수 없습니다.`);
}
// 파이프라인 결과를 통합 응답 형식으로 변환
const responseText = formatPipelineResponse(result);
console.log('🔍 getWaterInfoIntegrated response:', {
responseText,
hasDirectAnswer: !!result.direct_answer,
directAnswer: result.direct_answer
});
// ChatGPT가 추가 질문하지 않도록 완전한 응답 구조로 반환
return {
status: 'success',
summary: result.summary || `${result.found_stations}개 관측소 발견`,
direct_answer: responseText,
detailed_data: {
primary_station: result.stations[0] || {},
related_stations: result.stations.slice(1, 4) || []
},
timestamp: result.timestamp,
no_additional_query_needed: true
};
} catch (error: any) {
return createErrorResponse(`데이터 조회 중 오류가 발생했습니다: ${error.message}`);
}
}
function formatPipelineResponse(result: PipelineResult): string {
if (!result || result.found_stations === 0 || !result.stations || result.stations.length === 0) {
return result.direct_answer || '관측소 정보를 찾을 수 없습니다.';
}
const primaryStation = result.stations[0];
const stationData = primaryStation.current_data || {};
const dataType = result.query_analysis?.dataType || 'waterlevel';
// 데이터 타입에 따라 동적 제목 생성
let title: string;
if (dataType === 'rainfall') {
title = `🌧️ ${primaryStation.name} 실시간 강수량 정보`;
} else if (dataType === 'dam') {
title = `댐 ${primaryStation.name} 실시간 댐 정보`;
} else {
title = `🌊 ${primaryStation.name} 실시간 수위 정보`;
}
let directAnswer = result.direct_answer || '데이터를 요약할 수 없습니다.';
// 상세 정보 포맷팅
let details = '📈 **상세 정보**:\n';
if (dataType === 'rainfall') {
details += `• 강수량: ${stationData.rainfall || 'N/A'}\n`;
} else if (dataType === 'waterlevel') {
details += `• 수위: ${stationData.water_level || 'N/A'}\n`;
} else if (dataType === 'dam') {
details += `• 수위: ${stationData.water_level || 'N/A'}\n`;
details += `• 저수율: ${stationData.storage_rate || 'N/A'}\n`;
details += `• 유입량: ${stationData.inflow || 'N/A'}\n`;
details += `• 방류량: ${stationData.outflow || 'N/A'}\n`;
}
details += `• 상태: ${stationData.status || '정상'}\n`;
if (stationData.trend) {
details += `• 추세: ${stationData.trend}\n`;
}
details += `• 최종 업데이트: ${stationData.last_updated ? new Date(stationData.last_updated).toLocaleString('ko-KR') : 'N/A'}\n`;
// 관련 관측소 포맷팅
let related = '';
if (result.stations.length > 1) {
related = '\n🔗 **관련 관측소**:\n';
result.stations.slice(1, 4).forEach(st => {
related += `• ${st.name} (코드: ${st.code})\n`;
});
}
const timestamp = `\n⏰ 조회 시간: ${new Date(result.timestamp).toLocaleString('ko-KR')}`;
return `${title}\n\n📊 **현재 상태**: ${directAnswer}\n\n${details}${related}${timestamp}`;
}
// StationMapper 초기화 함수
async function initializeStationMapper(): Promise<void> {
if (!isMapperInitialized) {
console.log('🔄 StationMapper 초기화 시작...');
await stationMapper.initializeMapping();
isMapperInitialized = true;
console.log('✅ StationMapper 초기화 완료');
}
}
function findStationCode(query: string): string | null {
// StationMapper를 사용하여 관측소 코드 찾기
const result = stationMapper.findStationCode(query);
return result ? result.code : null;
}
function createIntegratedResponse(stationName: string, stationCode: string, data: any) {
const currentLevel = `${data.water_level.toFixed(1)}m`;
const storageRate = calculateStorageRate(data.water_level);
const status = determineStatus(data.water_level);
const trend = determineTrend(data.water_level);
const lastUpdated = new Date(data.obs_time).toLocaleString('ko-KR');
return {
status: 'success',
summary: `${stationName} 현재 수위는 ${currentLevel}입니다 (저수율 ${storageRate})`,
direct_answer: `${stationName}의 현재 수위는 ${currentLevel}이며, 저수율 ${storageRate}로 ${status} 상태입니다.`,
detailed_data: {
primary_station: {
name: stationName,
code: stationCode,
current_level: currentLevel,
storage_rate: storageRate,
status: status,
trend: trend,
last_updated: lastUpdated
},
related_stations: getRelatedStations(stationName)
},
timestamp: new Date().toISOString()
};
}
function createErrorResponse(message: string) {
return {
status: 'error',
summary: message,
direct_answer: message,
detailed_data: {
primary_station: {
name: '',
code: ''
}
},
timestamp: new Date().toISOString()
};
}
function calculateStorageRate(waterLevel: number): string {
// 간단한 저수율 계산 (실제로는 더 복잡한 공식 필요)
const baseLevel = 100; // 기준 수위
const maxLevel = 150; // 최대 수위
const rate = Math.min(100, Math.max(0, ((waterLevel - baseLevel) / (maxLevel - baseLevel)) * 100));
return `${rate.toFixed(1)}%`;
}
function determineStatus(waterLevel: number): string {
if (waterLevel < 110) return '낮음';
if (waterLevel > 140) return '높음';
return '정상';
}
function determineTrend(waterLevel: number): string {
// 실제로는 이전 데이터와 비교해야 함
const random = Math.random();
if (random < 0.3) return '상승';
if (random < 0.6) return '하강';
return '안정';
}
function getRelatedStations(stationName: string): Array<{name: string, code: string, current_level?: string, status?: string}> {
// 관련 관측소 반환 (간단한 예시)
const related = [];
if (stationName.includes('댐')) {
related.push({ name: '소양댐', code: '1018681' });
related.push({ name: '충주댐', code: '1018682' });
} else if (stationName.includes('대교')) {
related.push({ name: '한강대교', code: '1018690' });
related.push({ name: '잠실대교', code: '1018691' });
}
return related;
}
function getDemoWaterLevelData(obsCode: string): any[] {
// 관측소별 현실적인 수위 데이터 (댐, 수위관측소, 우량관측소 포함)
const stationData: Record<string, number> = {
// 주요 댐들
'1018680': 120.5, // 대청댐
'1018681': 115.2, // 소양댐
'1018682': 118.8, // 충주댐
'1018683': 125.3, // 안동댐
'1018684': 122.1, // 임하댐
'1018685': 128.7, // 합천댐
'1018686': 116.9, // 영주댐
'1018687': 119.4, // 보령댐
'1018688': 124.6, // 대암댐
'1018689': 117.8, // 춘천댐
'1018690': 123.2, // 팔당댐
'1018691': 115.7, // 의암댐
'1018692': 121.3, // 청평댐
'1018693': 118.9, // 화천댐
// 한강 수위관측소들
'1018700': 8.5, // 한강대교
'1018701': 7.2, // 잠실대교
'1018702': 6.8, // 성산대교
'1018703': 7.5, // 반포대교
'1018704': 6.9, // 동작대교
'1018705': 7.1, // 한남대교
'1018706': 6.7, // 청담대교
'1018707': 7.3, // 영동대교
'1018708': 8.1, // 구리대교
'1018709': 7.8, // 팔당대교
'1018710': 8.3, // 양평대교
'1018711': 7.9, // 여주대교
'1018712': 8.0, // 이천대교
'1018713': 7.6, // 안성대교
'1018714': 7.4, // 평택대교
'1018715': 7.7, // 아산대교
'1018716': 7.8, // 천안대교
'1018717': 8.2, // 공주대교
'1018718': 7.9, // 부여대교
'1018719': 8.1, // 논산대교
'1018720': 7.5, // 익산대교
'1018721': 7.3, // 전주대교
'1018722': 7.1, // 군산대교
'1018723': 7.4, // 김제대교
'1018724': 7.6, // 정읍대교
'1018725': 7.8, // 순창대교
'1018726': 7.2, // 남원대교
'1018727': 7.0, // 구례대교
'1018728': 6.9, // 곡성대교
'1018729': 6.8, // 순천대교
'1018730': 6.7, // 여수대교
'1018731': 6.6, // 광양대교
// 낙동강 수위관측소들
'1018761': 5.2, // 낙동강
'1018762': 5.1, // 낙동강대교
'1018763': 5.0, // 구포대교
'1018764': 4.9, // 사상대교
'1018765': 4.8, // 금정대교
'1018766': 4.7, // 동래대교
'1018767': 4.6, // 해운대대교
'1018768': 4.5, // 기장대교
'1018769': 4.4, // 울산대교
'1018770': 4.3, // 양산대교
'1018771': 4.2, // 밀양대교
'1018772': 4.1, // 창녕대교
'1018773': 4.0, // 의령대교
'1018774': 3.9, // 합천대교
'1018775': 3.8, // 거창대교
'1018776': 3.7, // 함양대교
'1018777': 3.6, // 산청대교
'1018778': 3.5, // 하동대교
'1018779': 3.4, // 남해대교
'1018780': 3.3, // 통영대교
'1018781': 3.2, // 거제대교
'1018782': 3.1, // 고성대교
// 금강 수위관측소들
'1018783': 6.5, // 금강
'1018784': 6.4, // 금강대교
'1018785': 6.3, // 공주대교
'1018786': 6.2, // 부여대교
'1018787': 6.1, // 논산대교
'1018788': 6.0, // 익산대교
'1018789': 5.9, // 전주대교
'1018790': 5.8, // 군산대교
'1018791': 5.7, // 김제대교
'1018792': 5.6, // 정읍대교
'1018793': 5.5, // 순창대교
'1018794': 5.4, // 남원대교
'1018795': 5.3, // 구례대교
'1018796': 5.2, // 곡성대교
'1018797': 5.1, // 순천대교
'1018798': 5.0, // 여수대교
'1018799': 4.9, // 광양대교
// 영산강 수위관측소들
'1018800': 4.5, // 영산강
'1018801': 4.4, // 영산강대교
'1018802': 4.3, // 나주대교
'1018803': 4.2, // 함평대교
'1018804': 4.1, // 영광대교
'1018805': 4.0, // 장성대교
'1018806': 3.9, // 담양대교
'1018807': 3.8, // 곡성대교
'1018808': 3.7, // 순천대교
'1018809': 3.6, // 여수대교
'1018810': 3.5, // 광양대교
// 섬진강 수위관측소들
'1018811': 3.2, // 섬진강
'1018812': 3.1, // 섬진강대교
'1018813': 3.0, // 구례대교
'1018814': 2.9, // 곡성대교
'1018815': 2.8, // 순천대교
'1018816': 2.7, // 여수대교
'1018817': 2.6, // 광양대교
// 임진강 수위관측소들
'1018818': 2.5, // 임진강
'1018819': 2.4, // 임진강대교
'1018820': 2.3, // 파주대교
'1018821': 2.2, // 연천대교
'1018822': 2.1, // 철원대교
'1018823': 2.0, // 화천대교
'1018824': 1.9, // 춘천대교
// 우량관측소들 (강우량 데이터)
'1018825': 0.0, // 서울우량관측소
'1018826': 0.0, // 부산우량관측소
'1018827': 0.0, // 대구우량관측소
'1018828': 0.0, // 인천우량관측소
'1018829': 0.0, // 광주우량관측소
'1018830': 0.0, // 대전우량관측소
'1018831': 0.0, // 울산우량관측소
'1018832': 0.0, // 경기우량관측소
'1018833': 0.0, // 강원우량관측소
'1018834': 0.0, // 충북우량관측소
'1018835': 0.0, // 충남우량관측소
'1018836': 0.0, // 전북우량관측소
'1018837': 0.0, // 전남우량관측소
'1018838': 0.0, // 경북우량관측소
'1018839': 0.0, // 경남우량관측소
'1018840': 0.0, // 제주우량관측소
// 지역별 수위관측소들
'1018841': 8.5, // 서울수위관측소
'1018842': 5.2, // 부산수위관측소
'1018843': 4.8, // 대구수위관측소
'1018844': 7.2, // 인천수위관측소
'1018845': 4.5, // 광주수위관측소
'1018846': 6.8, // 대전수위관측소
'1018847': 4.4, // 울산수위관측소
'1018848': 7.8, // 경기수위관측소
'1018849': 6.2, // 강원수위관측소
'1018850': 5.9, // 충북수위관측소
'1018851': 6.1, // 충남수위관측소
'1018852': 5.7, // 전북수위관측소
'1018853': 4.3, // 전남수위관측소
'1018854': 4.6, // 경북수위관측소
'1018855': 4.2, // 경남수위관측소
'1018856': 2.8, // 제주수위관측소
};
const waterLevel = stationData[obsCode] || (Math.random() * 10 + 5);
return [
{
obs_code: obsCode,
obs_time: new Date().toISOString(),
water_level: waterLevel,
unit: 'm',
},
];
}
function formatIntegratedResponse(response: any): string {
if (response.status === 'error') {
return `❌ ${response.direct_answer}`;
}
const { primary_station, related_stations } = response.detailed_data;
let formatted = `🌊 **${primary_station.name} 실시간 수위 정보**\n\n`;
formatted += `📊 **현재 상태**: ${response.direct_answer}\n\n`;
formatted += `📈 **상세 정보**:\n`;
formatted += `• 수위: ${primary_station.current_level}\n`;
formatted += `• 저수율: ${primary_station.storage_rate}\n`;
formatted += `• 상태: ${primary_station.status}\n`;
formatted += `• 추세: ${primary_station.trend}\n`;
formatted += `• 최종 업데이트: ${primary_station.last_updated}\n`;
if (related_stations && related_stations.length > 0) {
formatted += `\n🔗 **관련 관측소**:\n`;
related_stations.forEach((station: any) => {
formatted += `• ${station.name} (코드: ${station.code})\n`;
});
}
formatted += `\n⏰ 조회 시간: ${new Date(response.timestamp).toLocaleString('ko-KR')}`;
return formatted;
}
// 메인 핸들러
export const handler: Handler = async (event) => {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json'
};
if (event.httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: '' };
}
try {
const body = JSON.parse(event.body || '{}');
const { method, params, id } = body;
let result;
switch (method) {
case 'initialize':
result = await mcpInitialize();
break;
case 'tools/list':
result = await mcpToolsList();
break;
case 'tools/call':
result = await mcpToolsCall(params.name, params.arguments);
break;
default:
result = mcpError(id, `Unknown method: ${method}`);
}
// ID가 있으면 추가
if (id !== undefined) {
(result as any).id = id;
}
return {
statusCode: 200,
headers,
body: JSON.stringify(result)
};
} catch (error: any) {
return {
statusCode: 500,
headers,
body: JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32700,
message: error.message || 'Parse error'
}
})
};
}
};