@devyhan/naver-maps-mcp
Version:
MCP 서버 for 네이버 지도 API
365 lines (337 loc) • 10.9 kB
JavaScript
/**
* 네이버 지도 MCP 웹 서버
* 네이버 Maps API를 MCP 프로토콜로 제공하는 웹 서버 구현입니다.
*/
import express from 'express';
import cors from 'cors';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import naverMapsClient from './src/api/naverMapsClient.js';
import logger from './src/utils/logger.js';
import config from './src/utils/config.js';
// Express 앱 생성
const app = express();
app.use(cors());
app.use(express.json());
// 포트 설정 (Heroku에서는 process.env.PORT를 사용)
const PORT = process.env.PORT || 3000;
// MCP 서버 인스턴스 생성
function createMcpServer() {
const server = new Server({
name: 'naver-maps-mcp',
version: '1.0.0'
}, {
capabilities: {
tools: {}
}
});
// 도구 목록 제공
server.setRequestHandler(ListToolsRequestSchema, async () => {
logger.debug('도구 목록 요청 처리');
return {
tools: [
// 지오코딩 도구
{
name: 'geocode',
description: '주소를 좌표(위도, 경도)로 변환합니다.',
inputSchema: {
type: 'object',
properties: {
address: {
type: 'string',
description: '검색할 주소'
},
filter: {
type: 'string',
description: '지역 필터링 (구, 동 등)'
}
},
required: ['address']
}
},
// 역지오코딩 도구
{
name: 'reverseGeocode',
description: '좌표(위도, 경도)를 주소로 변환합니다.',
inputSchema: {
type: 'object',
properties: {
latitude: {
type: 'number',
description: '위도'
},
longitude: {
type: 'number',
description: '경도'
},
coords_type: {
type: 'string',
description: '좌표계 타입',
enum: ['latlng', 'utmk', 'tm128', 'epsg', 'naver', 'bessel'],
default: 'latlng'
}
},
required: ['latitude', 'longitude']
}
},
// 장소 검색 도구
{
name: 'searchPlaces',
description: '키워드로 장소를 검색합니다.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '검색 키워드'
},
coordinate: {
type: 'object',
description: '검색 중심 좌표',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
}
},
radius: {
type: 'number',
description: '검색 반경 (미터 단위)'
}
},
required: ['query']
}
},
// 경로 탐색 도구 (방향 5)
{
name: 'getDirections',
description: '출발지와 목적지 사이의 최적 경로를 제공합니다.',
inputSchema: {
type: 'object',
properties: {
start: {
type: 'object',
description: '출발 좌표',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
},
required: ['latitude', 'longitude']
},
goal: {
type: 'object',
description: '도착 좌표',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
},
required: ['latitude', 'longitude']
},
option: {
type: 'string',
description: '경로 옵션',
enum: ['trafast', 'tracomfort', 'traoptimal', 'traavoidtoll'],
default: 'trafast'
},
lang: {
type: 'string',
description: '안내 언어',
enum: ['ko', 'en', 'ja', 'zh'],
default: 'ko'
}
},
required: ['start', 'goal']
}
},
// 경유지 포함 경로 탐색 도구 (방향 15)
{
name: 'getDirectionsWithWaypoints',
description: '경유지를 포함한 최적 경로를 제공합니다.',
inputSchema: {
type: 'object',
properties: {
start: {
type: 'object',
description: '출발 좌표',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
},
required: ['latitude', 'longitude']
},
goal: {
type: 'object',
description: '도착 좌표',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
},
required: ['latitude', 'longitude']
},
waypoints: {
type: 'array',
description: '경유지 좌표 목록',
items: {
type: 'object',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
},
required: ['latitude', 'longitude']
}
},
option: {
type: 'string',
description: '경로 옵션',
enum: ['trafast', 'tracomfort', 'traoptimal', 'traavoidtoll'],
default: 'trafast'
}
},
required: ['start', 'goal']
}
}
]
};
});
// 도구 호출 처리
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
logger.debug(`도구 호출: ${name}`, args);
try {
let result;
// 각 도구별 구현
switch (name) {
case 'geocode':
result = await naverMapsClient.geocode(args.address, {
filter: args.filter
});
break;
case 'reverseGeocode':
result = await naverMapsClient.reverseGeocode(
args.latitude,
args.longitude,
{ coords_type: args.coords_type }
);
break;
case 'searchPlaces':
// 실제 구현은 네이버 API 문서 확인 후 추가 필요
// 현재는 더미 응답 반환
result = {
places: [
{
name: '검색 결과 예시',
address: '서울특별시 강남구',
location: { latitude: 37.5, longitude: 127.0 }
}
]
};
break;
case 'getDirections':
result = await naverMapsClient.getDirections5(
args.start,
args.goal,
{
option: args.option,
lang: args.lang
}
);
break;
case 'getDirectionsWithWaypoints':
result = await naverMapsClient.getDirections15(
args.start,
args.goal,
args.waypoints || [],
{ option: args.option }
);
break;
default:
throw new Error(`알 수 없는 도구: ${name}`);
}
// 결과 반환
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
logger.error(`도구 실행 오류: ${name}`, error);
// 오류 결과 반환
return {
isError: true,
content: [
{
type: 'text',
text: `오류: ${error.message || '알 수 없는 오류'}`
}
]
};
}
});
return server;
}
// 최소한의 환경 변수 검증 로직
function validateConfig() {
// 프로덕션 환경에서는 실제 API 키가 설정되어 있어야 함
if (process.env.NODE_ENV === 'production') {
if (!process.env.NAVER_CLIENT_ID || !process.env.NAVER_CLIENT_SECRET) {
logger.warn('프로덕션 환경에서 네이버 API 키가 설정되지 않았습니다. 더미 데이터가 반환될 수 있습니다.');
}
}
return true;
}
// SSE 엔드포인트 설정
app.get('/mcp', (req, res) => {
if (!validateConfig()) {
res.status(500).send('Server configuration error');
return;
}
logger.info('SSE 연결 수립 중...');
const mcpServer = createMcpServer();
const transport = new SSEServerTransport('/mcp-messages', res);
mcpServer.connect(transport).catch(error => {
logger.error('MCP 서버 연결 오류:', error);
});
});
// 메시지 엔드포인트 설정
app.post('/mcp-messages', (req, res) => {
// SSEServerTransport에서 자동으로 처리
logger.debug('메시지 수신됨');
});
// 홈페이지
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<title>Naver Maps MCP Server</title>
<style>
body { font-family: system-ui, sans-serif; margin: 40px; line-height: 1.6; }
h1 { color:
.container { max-width: 800px; margin: 0 auto; }
.info { background:
code { background:
</style>
</head>
<body>
<div class="container">
<h1>Naver Maps MCP Server</h1>
<div class="info">
<p>This server provides Naver Maps API functionality through the Model Context Protocol (MCP).</p>
<p>MCP Endpoint: <code>${req.protocol}://${req.get('host')}/mcp</code></p>
<p>For more information, visit the <a href="https://github.com/devyhan/naver-maps-mcp" target="_blank">GitHub repository</a>.</p>
</div>
</div>
</body>
</html>
`);
});
// 서버 시작
app.listen(PORT, () => {
logger.info(`네이버 지도 MCP 서버가 http://localhost:${PORT}에서 실행 중입니다`);
logger.info(`MCP 엔드포인트: http://localhost:${PORT}/mcp`);
});