index.js•12 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import fetch from 'node-fetch';
class NASAServer {
constructor() {
this.server = new Server(
{
name: 'nasa-api-extension',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.apiKey = process.env.NASA_API_KEY || 'DEMO_KEY';
this.baseUrl = 'https://api.nasa.gov';
this.setupToolHandlers();
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_apod',
description: 'NASA의 오늘의 천체 사진(Astronomy Picture of the Day)을 가져옵니다',
inputSchema: {
type: 'object',
properties: {
date: {
type: 'string',
description: 'YYYY-MM-DD 형식의 날짜 (선택사항, 기본값: 오늘)',
},
hd: {
type: 'boolean',
description: '고해상도 이미지 여부 (기본값: false)',
default: false,
},
},
},
},
{
name: 'get_mars_rover_photos',
description: '화성 로버의 사진을 가져옵니다',
inputSchema: {
type: 'object',
properties: {
rover: {
type: 'string',
enum: ['curiosity', 'opportunity', 'spirit', 'perseverance'],
description: '로버 이름',
default: 'curiosity',
},
sol: {
type: 'number',
description: '화성 일수 (Sol)',
default: 1000,
},
camera: {
type: 'string',
description: '카메라 타입 (FHAZ, RHAZ, MAST, CHEMCAM, MAHLI, MARDI, NAVCAM)',
},
page: {
type: 'number',
description: '페이지 번호',
default: 1,
},
},
required: ['rover'],
},
},
{
name: 'get_neo_feed',
description: '근지구 천체(Near Earth Objects) 정보를 가져옵니다',
inputSchema: {
type: 'object',
properties: {
start_date: {
type: 'string',
description: '시작 날짜 (YYYY-MM-DD)',
},
end_date: {
type: 'string',
description: '종료 날짜 (YYYY-MM-DD)',
},
},
},
},
{
name: 'search_nasa_images',
description: 'NASA 이미지 및 비디오 라이브러리에서 검색합니다',
inputSchema: {
type: 'object',
properties: {
q: {
type: 'string',
description: '검색 쿼리',
},
media_type: {
type: 'string',
enum: ['image', 'video', 'audio'],
description: '미디어 타입',
default: 'image',
},
page: {
type: 'number',
description: '페이지 번호',
default: 1,
},
},
required: ['q'],
},
},
{
name: 'get_earth_imagery',
description: 'NASA의 지구 이미지 API를 통해 위성 이미지를 가져옵니다',
inputSchema: {
type: 'object',
properties: {
lat: {
type: 'number',
description: '위도',
},
lon: {
type: 'number',
description: '경도',
},
date: {
type: 'string',
description: '날짜 (YYYY-MM-DD)',
},
dim: {
type: 'number',
description: '이미지 크기 (0.03 ~ 0.5)',
default: 0.15,
},
},
required: ['lat', 'lon'],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_apod':
return await this.getAPOD(args);
case 'get_mars_rover_photos':
return await this.getMarsRoverPhotos(args);
case 'get_neo_feed':
return await this.getNEOFeed(args);
case 'search_nasa_images':
return await this.searchNASAImages(args);
case 'get_earth_imagery':
return await this.getEarthImagery(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`알 수 없는 도구: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`도구 실행 중 오류 발생: ${error.message}`
);
}
});
}
async getAPOD(args) {
const { date, hd = false } = args || {};
const params = new URLSearchParams({
api_key: this.apiKey,
hd: hd.toString(),
});
if (date) {
params.append('date', date);
}
const response = await fetch(`${this.baseUrl}/planetary/apod?${params}`);
const data = await response.json();
if (!response.ok) {
throw new Error(`NASA API 오류: ${data.error?.message || '알 수 없는 오류'}`);
}
return {
content: [
{
type: 'text',
text: `**${data.title}** (${data.date})
${data.explanation}
${data.media_type === 'image' ? `이미지 URL: ${data.url}` : `비디오 URL: ${data.url}`}
${data.copyright ? `저작권: ${data.copyright}` : ''}`,
},
],
};
}
async getMarsRoverPhotos(args) {
const { rover, sol = 1000, camera, page = 1 } = args;
const params = new URLSearchParams({
api_key: this.apiKey,
sol: sol.toString(),
page: page.toString(),
});
if (camera) {
params.append('camera', camera);
}
const response = await fetch(`${this.baseUrl}/mars-photos/api/v1/rovers/${rover}/photos?${params}`);
const data = await response.json();
if (!response.ok) {
throw new Error(`NASA API 오류: ${data.error?.message || '알 수 없는 오류'}`);
}
const photos = data.photos.slice(0, 10); // 최대 10개 사진만 반환
return {
content: [
{
type: 'text',
text: `**${rover.toUpperCase()} 로버 사진들** (Sol ${sol})
총 ${data.photos.length}개의 사진을 찾았습니다. 처음 ${photos.length}개를 보여드립니다:
${photos.map((photo, index) => `
${index + 1}. **사진 ID**: ${photo.id}
- **카메라**: ${photo.camera.full_name} (${photo.camera.name})
- **촬영일**: ${photo.earth_date}
- **이미지 URL**: ${photo.img_src}
`).join('')}`,
},
],
};
}
async getNEOFeed(args) {
const { start_date, end_date } = args || {};
const params = new URLSearchParams({
api_key: this.apiKey,
});
if (start_date) params.append('start_date', start_date);
if (end_date) params.append('end_date', end_date);
const response = await fetch(`${this.baseUrl}/neo/rest/v1/feed?${params}`);
const data = await response.json();
if (!response.ok) {
throw new Error(`NASA API 오류: ${data.error?.message || '알 수 없는 오류'}`);
}
const totalCount = data.element_count;
const dates = Object.keys(data.near_earth_objects);
let neoList = [];
dates.forEach(date => {
data.near_earth_objects[date].forEach(neo => {
neoList.push({
date,
name: neo.name,
id: neo.id,
diameter: neo.estimated_diameter.kilometers,
hazardous: neo.is_potentially_hazardous_asteroid,
close_approach: neo.close_approach_data[0],
});
});
});
return {
content: [
{
type: 'text',
text: `**근지구 천체 정보** (${start_date || '오늘'} ~ ${end_date || '7일 후'})
총 ${totalCount}개의 근지구 천체가 발견되었습니다:
${neoList.slice(0, 10).map((neo, index) => `
${index + 1}. **${neo.name}**
- **ID**: ${neo.id}
- **날짜**: ${neo.date}
- **지름**: ${neo.diameter.estimated_diameter_min.toFixed(3)} - ${neo.diameter.estimated_diameter_max.toFixed(3)} km
- **위험 여부**: ${neo.hazardous ? '위험' : '안전'}
- **최근접 거리**: ${parseFloat(neo.close_approach.miss_distance.kilometers).toLocaleString()} km
- **속도**: ${parseFloat(neo.close_approach.relative_velocity.kilometers_per_hour).toLocaleString()} km/h
`).join('')}`,
},
],
};
}
async searchNASAImages(args) {
const { q, media_type = 'image', page = 1 } = args;
const params = new URLSearchParams({
q,
media_type,
page: page.toString(),
});
const response = await fetch(`https://images-api.nasa.gov/search?${params}`);
const data = await response.json();
if (!response.ok) {
throw new Error(`NASA Images API 오류: ${data.reason || '알 수 없는 오류'}`);
}
const items = data.collection.items.slice(0, 10); // 최대 10개 결과만 반환
return {
content: [
{
type: 'text',
text: `**NASA 이미지 검색 결과** - "${q}"
총 ${data.collection.metadata.total_hits}개의 결과를 찾았습니다. 처음 ${items.length}개를 보여드립니다:
${items.map((item, index) => `
${index + 1}. **${item.data[0].title}**
- **설명**: ${item.data[0].description || '설명 없음'}
- **날짜**: ${item.data[0].date_created}
- **미디어 타입**: ${item.data[0].media_type}
- **이미지 URL**: ${item.links?.[0]?.href || '이미지 없음'}
- **NASA ID**: ${item.data[0].nasa_id}
`).join('')}`,
},
],
};
}
async getEarthImagery(args) {
const { lat, lon, date, dim = 0.15 } = args;
const params = new URLSearchParams({
lat: lat.toString(),
lon: lon.toString(),
dim: dim.toString(),
api_key: this.apiKey,
});
if (date) {
params.append('date', date);
}
const response = await fetch(`${this.baseUrl}/planetary/earth/imagery?${params}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(`NASA Earth Imagery API 오류: ${errorData.error?.message || '알 수 없는 오류'}`);
}
// 이미지 데이터를 직접 반환하는 대신 URL을 구성
const imageUrl = `${this.baseUrl}/planetary/earth/imagery?${params}`;
return {
content: [
{
type: 'text',
text: `**지구 위성 이미지**
위치: 위도 ${lat}, 경도 ${lon}
${date ? `날짜: ${date}` : '최신 이미지'}
이미지 크기: ${dim}
이미지 URL: ${imageUrl}
이 이미지는 NASA의 Landsat 8 위성에서 촬영된 지구 표면의 위성 이미지입니다.`,
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('NASA API MCP 서버가 시작되었습니다.');
}
}
const server = new NASAServer();
server.run().catch(console.error);