├── .env.example ├── src ├── resources.ts ├── utils │ ├── param-transformer.ts │ ├── env-setup.ts │ ├── api-client.ts │ └── manifest.ts ├── handlers │ ├── jpl │ │ ├── jd_cal.ts │ │ ├── fireball.ts │ │ ├── nhats.ts │ │ ├── horizons.ts │ │ ├── cad.ts │ │ ├── periodic_orbits.ts │ │ ├── sbdb.ts │ │ ├── horizons_file.ts │ │ ├── sentry.ts │ │ └── scout.ts │ ├── nasa │ │ ├── osdr_files.ts │ │ ├── donki.ts │ │ ├── apod.ts │ │ ├── earth.ts │ │ ├── gibs.ts │ │ ├── exoplanet.ts │ │ ├── firms.ts │ │ ├── eonet.ts │ │ ├── images.ts │ │ ├── mars_rover.ts │ │ ├── epic.ts │ │ ├── cmr.ts │ │ ├── neo.ts │ │ └── power.ts │ └── setup.ts └── tests │ ├── custom-client │ ├── simple-test.ts │ └── nasa-mcp-test.ts │ └── direct-api-test.ts ├── tsconfig.json ├── .gitignore ├── .eslintrc.json ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── LICENSE ├── package.json ├── docs └── inspector-test-examples.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | PORT=3000 3 | 4 | # NASA API Key (Get yours at https://api.nasa.gov/) 5 | NASA_API_KEY=YOUR_NASA_API_KEY 6 | -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- 1 | export interface Resource { 2 | name: string; 3 | mimeType: string; 4 | text?: string; 5 | blob?: Uint8Array; 6 | } 7 | 8 | // Central registry of resources 9 | export const resources = new Map(); 10 | 11 | // Add or replace a resource in the registry 12 | export function addResource(uri: string, resource: Resource) { 13 | resources.set(uri, resource); 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "lib": ["ES2020"], 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "**/*.test.ts"] 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | /npm-debug.log 4 | /yarn-error.log 5 | /yarn-debug.log 6 | 7 | # Build 8 | /dist 9 | /build 10 | 11 | # Environment 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | # IDE 19 | /.idea 20 | /.vscode 21 | /*.sublime-* 22 | 23 | # System Files 24 | .DS_Store 25 | Thumbs.db 26 | # Shell scripts with sensitive data 27 | *.sh 28 | start-sse-server.sh 29 | 30 | # Secrets 31 | *_key* 32 | *apikey* 33 | *secret* 34 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [], 6 | "rules": { 7 | "no-console": "off", 8 | "no-unused-vars": "off", 9 | "@typescript-eslint/no-unused-vars": "off", 10 | "@typescript-eslint/no-explicit-any": "off", 11 | "no-undef": "off", 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-inner-declarations": "off", 14 | "@typescript-eslint/no-this-alias": "off" 15 | }, 16 | "env": { 17 | "node": true 18 | } 19 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to npm Registry 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | - run: npm ci 21 | - run: npm publish 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '20.x' 20 | cache: 'npm' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Lint 29 | run: npm run lint 30 | 31 | - name: Test 32 | run: npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 ProgramComputer 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /src/utils/param-transformer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms parameter names from underscore format to hyphenated format. 3 | * This is needed because the MCP API accepts underscore format (typical for JS/Python), 4 | * but the external JPL APIs expect hyphenated format. 5 | * 6 | * @param params Original parameters with underscore format 7 | * @returns Transformed parameters with hyphenated format 8 | */ 9 | export function transformParamsToHyphenated(params: Record): Record { 10 | const transformed: Record = {}; 11 | 12 | for (const [key, value] of Object.entries(params)) { 13 | // Replace underscores with hyphens for parameter names 14 | const transformedKey = key.replace(/_/g, '-'); 15 | transformed[transformedKey] = value; 16 | } 17 | 18 | return transformed; 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@programcomputer/nasa-mcp-server", 3 | "version": "1.0.13", 4 | "description": "Model Context Protocol (MCP) server for NASA APIs", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "bin": { 10 | "nasa-mcp-server": "dist/index.js" 11 | }, 12 | "scripts": { 13 | "build": "npm install && tsc && shx chmod +x dist/*.js", 14 | "start": "node dist/index.js", 15 | "start:sse": "node dist/sse-server.js", 16 | "dev": "tsx watch src/index.ts", 17 | "dev:sse": "tsx watch src/sse-server.ts", 18 | "lint": "eslint .", 19 | "test": "jest", 20 | "direct-test": "ts-node src/tests/direct-api-test.ts", 21 | "custom-test": "ts-node src/tests/custom-client/nasa-mcp-test.ts", 22 | "simple-test": "ts-node src/tests/custom-client/simple-test.ts" 23 | }, 24 | "keywords": [ 25 | "nasa", 26 | "api", 27 | "mcp", 28 | "typescript" 29 | ], 30 | "author": "", 31 | "license": "ISC", 32 | "publishConfig": { 33 | "registry": "https://registry.npmjs.org", 34 | "access": "public" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/ProgramComputer/NASA-MCP-server.git" 39 | }, 40 | "dependencies": { 41 | "@anthropic-ai/sdk": "", 42 | "@modelcontextprotocol/sdk": "^1.9.0", 43 | "axios": "", 44 | "cors": "", 45 | "dotenv": "", 46 | "express": "", 47 | "zod": "" 48 | }, 49 | "devDependencies": { 50 | "@types/cors": "", 51 | "@types/express": "", 52 | "@types/jest": "", 53 | "@types/node": "", 54 | "@typescript-eslint/eslint-plugin": "^7.18.0", 55 | "@typescript-eslint/parser": "^7.18.0", 56 | "cross-env": "^7.0.3", 57 | "eslint": "^8.57.1", 58 | "jest": "", 59 | "shx": "^0.4.0", 60 | "ts-jest": "", 61 | "ts-node": "", 62 | "typed-rpc": "^6.1.1", 63 | "typescript": "" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/handlers/jpl/jd_cal.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | /** 6 | * Handler for JPL Julian Date Calendar Conversion API 7 | * 8 | * This API converts between Julian dates and calendar dates (UTC) 9 | * 10 | * @param args Request parameters 11 | * @returns API response 12 | */ 13 | export async function jdCalHandler(args: Record) { 14 | try { 15 | // Base URL for the JD Calendar API 16 | const baseUrl = 'https://ssd-api.jpl.nasa.gov/jd_cal.api'; 17 | 18 | // Validate parameters 19 | if (!args.jd && !args.cd) { 20 | return { 21 | content: [{ 22 | type: "text", 23 | text: "Error: Either a Julian date (jd) or calendar date (cd) must be provided." 24 | }], 25 | isError: true 26 | }; 27 | } 28 | 29 | // Transform parameter names from underscore to hyphenated format 30 | const transformedParams = transformParamsToHyphenated(args); 31 | 32 | // Make the API request 33 | const response = await axios.get(baseUrl, { params: transformedParams }); 34 | const data = response.data; 35 | 36 | // Add response to resources 37 | const resourceUri = `jpl://jd_cal/${args.jd || args.cd}`; 38 | addResource(resourceUri, { 39 | name: `Julian Date / Calendar Date Conversion: ${args.jd || args.cd}`, 40 | mimeType: "application/json", 41 | text: JSON.stringify(data, null, 2) 42 | }); 43 | 44 | // Format the response 45 | return { 46 | content: [{ 47 | type: "text", 48 | text: JSON.stringify(data, null, 2) 49 | }] 50 | }; 51 | } catch (error: any) { 52 | return { 53 | content: [{ 54 | type: "text", 55 | text: `Error accessing JPL Julian Date Calendar API: ${error.message}` 56 | }], 57 | isError: true 58 | }; 59 | } 60 | } 61 | 62 | // Export default for dynamic imports 63 | export default jdCalHandler; 64 | -------------------------------------------------------------------------------- /src/tests/custom-client/simple-test.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | import { z } from "zod"; 4 | 5 | async function testMcpServer() { 6 | console.log('Starting NASA MCP Server test...'); 7 | 8 | // Create a transport to communicate with the server 9 | const transport = new StdioClientTransport({ 10 | command: "node", 11 | args: ["dist/index.js"] 12 | }); 13 | 14 | // Create an MCP client 15 | const client = new Client( 16 | { 17 | name: "mcp-test-client", 18 | version: "1.0.0" 19 | }, 20 | { 21 | capabilities: { 22 | tools: {} 23 | } 24 | } 25 | ); 26 | 27 | try { 28 | // Connect to the server 29 | await client.connect(transport); 30 | console.log('Client connected successfully'); 31 | 32 | // Get tools manifest 33 | const toolsManifestSchema = z.object({ 34 | apis: z.array(z.object({ 35 | name: z.string(), 36 | id: z.string(), 37 | description: z.string().optional() 38 | })) 39 | }); 40 | 41 | const toolsRequest = await client.request( 42 | { method: "tools/manifest", params: {} }, 43 | toolsManifestSchema 44 | ); 45 | console.log('Tools manifest received:', JSON.stringify(toolsRequest, null, 2)); 46 | 47 | // Test APOD API 48 | console.log('Testing NASA APOD API...'); 49 | const apodResultSchema = z.any(); 50 | 51 | const apodResult = await client.request( 52 | { 53 | method: "nasa/apod", 54 | params: { date: "2023-01-01" } 55 | }, 56 | apodResultSchema 57 | ); 58 | console.log('APOD API test successful:', JSON.stringify(apodResult, null, 2)); 59 | 60 | console.log('All tests completed successfully!'); 61 | process.exit(0); 62 | } catch (error) { 63 | console.error('Test failed:', error); 64 | process.exit(1); 65 | } 66 | } 67 | 68 | // Run the test 69 | testMcpServer().catch(error => { 70 | console.error('Test failed:', error); 71 | process.exit(1); 72 | }); -------------------------------------------------------------------------------- /src/handlers/jpl/fireball.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | // Schema for validating JPL Fireball request parameters 6 | export const fireballParamsSchema = z.object({ 7 | date_min: z.string().optional(), 8 | date_max: z.string().optional(), 9 | energy_min: z.number().optional(), 10 | energy_max: z.number().optional(), 11 | impact_e_min: z.number().optional(), 12 | impact_e_max: z.number().optional(), 13 | vel_min: z.number().optional(), 14 | vel_max: z.number().optional(), 15 | alt_min: z.number().optional(), 16 | alt_max: z.number().optional(), 17 | req_loc: z.boolean().optional().default(false), 18 | req_alt: z.boolean().optional().default(false), 19 | req_vel: z.boolean().optional().default(false), 20 | req_vel_comp: z.boolean().optional().default(false), 21 | req_impact_e: z.boolean().optional().default(false), 22 | req_energy: z.boolean().optional().default(false), 23 | limit: z.number().optional().default(50) 24 | }); 25 | 26 | // Define the request parameter type based on the schema 27 | export type FireballParams = z.infer; 28 | 29 | /** 30 | * Make a request to NASA JPL's Fireball API 31 | */ 32 | export async function jplFireballHandler(params: FireballParams) { 33 | try { 34 | // Construct the Fireball API URL 35 | const url = 'https://ssd-api.jpl.nasa.gov/fireball.api'; 36 | 37 | // Transform parameter names from underscore to hyphenated format 38 | const transformedParams = transformParamsToHyphenated(params); 39 | 40 | // Make the request to the Fireball API 41 | const response = await axios.get(url, { params: transformedParams }); 42 | 43 | return { 44 | content: [ 45 | { 46 | type: "text", 47 | text: `Retrieved ${response.data.count || 0} fireball events.` 48 | }, 49 | { 50 | type: "text", 51 | text: JSON.stringify(response.data, null, 2) 52 | } 53 | ], 54 | isError: false 55 | }; 56 | } catch (error: any) { 57 | console.error('Error in JPL Fireball handler:', error); 58 | 59 | return { 60 | isError: true, 61 | content: [{ 62 | type: "text", 63 | text: `Error: ${error.message || 'An unexpected error occurred'}` 64 | }] 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/handlers/nasa/osdr_files.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | 4 | // Define expected parameters 5 | interface OsdrFilesParams { 6 | accession_number: string; 7 | } 8 | 9 | /** 10 | * Handler for NASA OSDR Data Files API 11 | * 12 | * Retrieves metadata about data files for a specific OSD study dataset, 13 | * including download links. 14 | * 15 | * @param args Request parameters conforming to OsdrFilesParams 16 | * @returns API response 17 | */ 18 | export async function osdrFilesHandler(args: OsdrFilesParams) { 19 | try { 20 | // Validate required parameters 21 | if (!args.accession_number) { 22 | throw new Error('Missing required parameter: accession_number must be provided.'); 23 | } 24 | 25 | // Base URL for the OSDR API 26 | const baseUrl = 'https://osdr.nasa.gov/osdr/data/osd/files'; 27 | const apiUrl = `${baseUrl}/${encodeURIComponent(args.accession_number)}`; 28 | 29 | // Make the API request using GET 30 | const response = await axios.get(apiUrl, { 31 | // OSDR API might require specific headers, e.g., Accept 32 | headers: { 33 | 'Accept': 'application/json' 34 | } 35 | }); 36 | const data = response.data; 37 | 38 | // Create a resource URI 39 | const resourceUri = `nasa://osdr/files/${encodeURIComponent(args.accession_number)}`; 40 | const resourceName = `OSDR Files for ${args.accession_number}`; 41 | 42 | // Add response to resources 43 | addResource(resourceUri, { 44 | name: resourceName, 45 | mimeType: "application/json", 46 | text: JSON.stringify(data, null, 2) 47 | }); 48 | 49 | // Format the response for MCP 50 | return { 51 | content: [{ 52 | type: "text", 53 | text: JSON.stringify(data, null, 2) 54 | }] 55 | }; 56 | } catch (error: any) { 57 | let errorMessage = `Error accessing NASA OSDR Files API: ${error.message}`; 58 | if (error.response) { 59 | // Include more detail from the API response if available 60 | errorMessage += `\nStatus: ${error.response.status}\nData: ${JSON.stringify(error.response.data)}`; 61 | } 62 | return { 63 | content: [{ 64 | type: "text", 65 | text: errorMessage 66 | }], 67 | isError: true 68 | }; 69 | } 70 | } 71 | 72 | // Export default for dynamic imports in index.ts 73 | export default osdrFilesHandler; 74 | -------------------------------------------------------------------------------- /src/handlers/jpl/nhats.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | /** 6 | * Handler for JPL NHATS API (Human-accessible NEOs data) 7 | * 8 | * This API provides data from the NASA/JPL NHATS database about Near-Earth Objects (NEOs) 9 | * that are potentially accessible by human missions. 10 | * 11 | * @param args Request parameters 12 | * @returns API response 13 | */ 14 | export async function nhatsHandler(args: Record) { 15 | try { 16 | // Base URL for the NHATS API 17 | const baseUrl = 'https://ssd-api.jpl.nasa.gov/nhats.api'; 18 | 19 | // Validate parameters if needed 20 | // Parameters are fairly flexible in this API, so minimal validation is needed 21 | 22 | // Transform parameter names from underscore to hyphenated format 23 | const transformedParams = transformParamsToHyphenated(args); 24 | 25 | // Make the API request 26 | const response = await axios.get(baseUrl, { params: transformedParams }); 27 | const data = response.data; 28 | 29 | // Create a resource URI that represents this query 30 | let resourceUri: string; 31 | 32 | if (args.des) { 33 | // Object mode - query for a specific object 34 | resourceUri = `jpl://nhats/object/${args.des}`; 35 | } else if (args.spk) { 36 | // Object mode - query for a specific object by SPK-ID 37 | resourceUri = `jpl://nhats/object/${args.spk}`; 38 | } else { 39 | // Summary mode - query for a list of objects with constraints 40 | const constraints = Object.entries(args) 41 | .map(([key, value]) => `${key}=${value}`) 42 | .join('&'); 43 | 44 | resourceUri = `jpl://nhats/summary${constraints ? '?' + constraints : ''}`; 45 | } 46 | 47 | // Add response to resources 48 | addResource(resourceUri, { 49 | name: args.des || args.spk 50 | ? `NHATS data for object: ${args.des || args.spk}` 51 | : 'NHATS summary data', 52 | mimeType: "application/json", 53 | text: JSON.stringify(data, null, 2) 54 | }); 55 | 56 | // Format the response 57 | return { 58 | content: [{ 59 | type: "text", 60 | text: JSON.stringify(data, null, 2) 61 | }] 62 | }; 63 | } catch (error: any) { 64 | return { 65 | content: [{ 66 | type: "text", 67 | text: `Error accessing JPL NHATS API: ${error.message}` 68 | }], 69 | isError: true 70 | }; 71 | } 72 | } 73 | 74 | // Export default for dynamic imports 75 | export default nhatsHandler; 76 | -------------------------------------------------------------------------------- /src/handlers/jpl/horizons.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | 4 | /** 5 | * Handler for JPL Horizons API 6 | * 7 | * This API provides ephemeris data for solar system objects (planets, moons, asteroids, comets, etc.). 8 | * 9 | * @param args Request parameters 10 | * @returns API response 11 | */ 12 | export async function horizonsHandler(args: Record) { 13 | try { 14 | // Base URL for the Horizons API 15 | const baseUrl = 'https://ssd.jpl.nasa.gov/api/horizons.api'; 16 | 17 | // Make the API request 18 | const response = await axios.get(baseUrl, { params: args }); 19 | const data = response.data; 20 | 21 | // Create a resource URI that represents this query 22 | let resourceUri = 'jpl://horizons'; 23 | let resourceName = 'JPL Horizons ephemeris data'; 24 | 25 | // Customize resource name based on the request type 26 | if (args.COMMAND) { 27 | // Add the object identifier to the resource URI 28 | resourceUri += `/object/${encodeURIComponent(args.COMMAND)}`; 29 | 30 | // Update resource name based on EPHEM_TYPE 31 | if (args.EPHEM_TYPE === 'OBSERVER') { 32 | resourceName = `${args.COMMAND} observer ephemeris data`; 33 | } else if (args.EPHEM_TYPE === 'VECTORS') { 34 | resourceName = `${args.COMMAND} vector ephemeris data`; 35 | } else if (args.EPHEM_TYPE === 'ELEMENTS') { 36 | resourceName = `${args.COMMAND} orbital elements data`; 37 | } else { 38 | resourceName = `${args.COMMAND} ephemeris data`; 39 | } 40 | 41 | // Add time range info if available 42 | if (args.START_TIME && args.STOP_TIME) { 43 | resourceName += ` (${args.START_TIME} to ${args.STOP_TIME})`; 44 | } 45 | } 46 | 47 | // Add query parameters to the URI 48 | const queryParams = Object.entries(args) 49 | .filter(([key]) => key !== 'COMMAND' && key !== 'format') 50 | .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`) 51 | .join('&'); 52 | 53 | if (queryParams) { 54 | resourceUri += `?${queryParams}`; 55 | } 56 | 57 | // Add response to resources 58 | addResource(resourceUri, { 59 | name: resourceName, 60 | mimeType: "application/json", 61 | text: JSON.stringify(data, null, 2) 62 | }); 63 | 64 | // Format the response 65 | return { 66 | content: [{ 67 | type: "text", 68 | text: JSON.stringify(data, null, 2) 69 | }] 70 | }; 71 | } catch (error: any) { 72 | return { 73 | content: [{ 74 | type: "text", 75 | text: `Error accessing JPL Horizons API: ${error.message}` 76 | }], 77 | isError: true 78 | }; 79 | } 80 | } 81 | 82 | // Export default for dynamic imports 83 | export default horizonsHandler; 84 | -------------------------------------------------------------------------------- /src/handlers/jpl/cad.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | /** 6 | * Handler for JPL SB Close Approach (CAD) API 7 | * 8 | * This API provides data about asteroid and comet close approaches to planets 9 | * in the past and future. 10 | * 11 | * @param args Request parameters 12 | * @returns API response 13 | */ 14 | export async function cadHandler(args: Record) { 15 | try { 16 | // Base URL for the CAD API 17 | const baseUrl = 'https://ssd-api.jpl.nasa.gov/cad.api'; 18 | 19 | // Validate parameters if needed 20 | // Parameters are fairly flexible in this API, so minimal validation is needed 21 | 22 | // Transform parameter names from underscore to hyphenated format 23 | const transformedParams = transformParamsToHyphenated(args); 24 | 25 | // Make the API request 26 | const response = await axios.get(baseUrl, { params: transformedParams }); 27 | const data = response.data; 28 | 29 | // Create a resource URI that represents this query 30 | let resourceUri: string; 31 | let resourceName: string; 32 | 33 | if (args.des) { 34 | // Query for a specific object 35 | resourceUri = `jpl://cad/object/${args.des}`; 36 | resourceName = `Close approaches for object ${args.des}`; 37 | } else if (args.spk) { 38 | // Query for a specific object by SPK-ID 39 | resourceUri = `jpl://cad/object/${args.spk}`; 40 | resourceName = `Close approaches for object ${args.spk}`; 41 | } else { 42 | // Query for close approaches with constraints 43 | const constraints = Object.entries(args) 44 | .map(([key, value]) => `${key}=${value}`) 45 | .join('&'); 46 | 47 | resourceUri = `jpl://cad/list${constraints ? '?' + constraints : ''}`; 48 | 49 | // Create a readable name based on date range and body 50 | const dateMin = args['date_min'] || 'now'; 51 | const dateMax = args['date_max'] || '+60'; 52 | const body = args.body || 'Earth'; 53 | 54 | resourceName = `Close approaches to ${body} from ${dateMin} to ${dateMax}`; 55 | } 56 | 57 | // Add response to resources 58 | addResource(resourceUri, { 59 | name: resourceName, 60 | mimeType: "application/json", 61 | text: JSON.stringify(data, null, 2) 62 | }); 63 | 64 | // Format the response 65 | return { 66 | content: [{ 67 | type: "text", 68 | text: JSON.stringify(data, null, 2) 69 | }] 70 | }; 71 | } catch (error: any) { 72 | return { 73 | content: [{ 74 | type: "text", 75 | text: `Error accessing JPL SB Close Approach API: ${error.message}` 76 | }], 77 | isError: true 78 | }; 79 | } 80 | } 81 | 82 | // Export default for dynamic imports 83 | export default cadHandler; 84 | -------------------------------------------------------------------------------- /src/handlers/nasa/donki.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { nasaApiRequest } from '../../utils/api-client'; 3 | import { DonkiParams } from '../setup'; 4 | import { addResource } from '../../resources'; 5 | 6 | /** 7 | * Handle requests for NASA's Space Weather Database Of Notifications, Knowledge, Information (DONKI) API 8 | */ 9 | export async function nasaDonkiHandler(params: DonkiParams) { 10 | try { 11 | const { type, startDate, endDate } = params; 12 | 13 | // Map the type to the appropriate endpoint 14 | const typeEndpoints: Record = { 15 | cme: '/DONKI/CME', 16 | cmea: '/DONKI/CMEAnalysis', 17 | gst: '/DONKI/GST', 18 | ips: '/DONKI/IPS', 19 | flr: '/DONKI/FLR', 20 | sep: '/DONKI/SEP', 21 | mpc: '/DONKI/MPC', 22 | rbe: '/DONKI/RBE', 23 | hss: '/DONKI/HSS', 24 | wsa: '/DONKI/WSAEnlilSimulations', 25 | notifications: '/DONKI/notifications' 26 | }; 27 | 28 | const endpoint = typeEndpoints[type.toLowerCase()]; 29 | 30 | // Validate that the endpoint exists for the given type 31 | if (!endpoint) { 32 | return { 33 | isError: true, 34 | content: [{ 35 | type: "text", 36 | text: `Error: Invalid DONKI type "${type}". Valid types are: ${Object.keys(typeEndpoints).join(', ')}` 37 | }] 38 | }; 39 | } 40 | 41 | const queryParams: Record = {}; 42 | 43 | // Add date parameters if provided 44 | if (startDate) queryParams.startDate = startDate; 45 | if (endDate) queryParams.endDate = endDate; 46 | 47 | // Call the NASA DONKI API 48 | const result = await nasaApiRequest(endpoint, queryParams); 49 | 50 | // Create a resource ID and register the resource 51 | const dateParams = []; 52 | if (startDate) dateParams.push(`start=${startDate}`); 53 | if (endDate) dateParams.push(`end=${endDate}`); 54 | 55 | const resourceId = `nasa://donki/${type}${dateParams.length > 0 ? '?' + dateParams.join('&') : ''}`; 56 | 57 | addResource(resourceId, { 58 | name: `DONKI ${type.toUpperCase()} Space Weather Data${startDate ? ` from ${startDate}` : ''}${endDate ? ` to ${endDate}` : ''}`, 59 | mimeType: 'application/json', 60 | text: JSON.stringify(result, null, 2) 61 | }); 62 | 63 | // Return the confirmation message and the actual data 64 | return { 65 | content: [ 66 | { 67 | type: "text", 68 | text: `Retrieved DONKI ${type.toUpperCase()} space weather data${startDate ? ` from ${startDate}` : ''}${endDate ? ` to ${endDate}` : ''}.` 69 | }, 70 | { 71 | type: "text", 72 | text: JSON.stringify(result, null, 2) 73 | } 74 | ], 75 | isError: false 76 | }; 77 | } catch (error: any) { 78 | console.error('Error in DONKI handler:', error); 79 | 80 | return { 81 | isError: true, 82 | content: [{ 83 | type: "text", 84 | text: `Error: ${error.message || 'An unexpected error occurred'}` 85 | }] 86 | }; 87 | } 88 | } 89 | 90 | // Export the handler function directly as default 91 | export default nasaDonkiHandler; 92 | -------------------------------------------------------------------------------- /src/handlers/jpl/periodic_orbits.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | // Define expected parameters based on documentation 6 | // Required: sys, family 7 | // Optional: libr, branch, periodmin, periodmax, periodunits, jacobimin, jacobimax, stabmin, stabmax 8 | interface PeriodicOrbitParams { 9 | sys: string; 10 | family: string; 11 | libr?: number; 12 | branch?: string; 13 | periodmin?: number; 14 | periodmax?: number; 15 | periodunits?: string; 16 | jacobimin?: number; 17 | jacobimax?: number; 18 | stabmin?: number; 19 | stabmax?: number; 20 | } 21 | 22 | /** 23 | * Handler for JPL Three-Body Periodic Orbits API 24 | * 25 | * Fetches data on periodic orbits in specified three-body systems. 26 | * 27 | * @param args Request parameters conforming to PeriodicOrbitParams 28 | * @returns API response 29 | */ 30 | export async function periodicOrbitsHandler(args: PeriodicOrbitParams) { 31 | try { 32 | // Validate required parameters 33 | if (!args.sys || !args.family) { 34 | throw new Error('Missing required parameters: sys and family must be provided.'); 35 | } 36 | 37 | // Base URL for the Periodic Orbits API 38 | const baseUrl = 'https://ssd-api.jpl.nasa.gov/periodic_orbits.api'; 39 | 40 | // Transform parameter names from underscore to hyphenated format 41 | const transformedParams = transformParamsToHyphenated(args); 42 | 43 | // Make the API request using GET with parameters 44 | const response = await axios.get(baseUrl, { params: transformedParams }); 45 | const data = response.data; 46 | 47 | // Create a resource URI 48 | // Example: jpl://periodic-orbits?sys=earth-moon&family=halo&libr=1&branch=N 49 | let resourceUri = `jpl://periodic-orbits?sys=${encodeURIComponent(args.sys)}&family=${encodeURIComponent(args.family)}`; 50 | let resourceName = `Periodic Orbits: ${args.sys} / ${args.family}`; 51 | if (args.libr) { 52 | resourceUri += `&libr=${args.libr}`; 53 | resourceName += ` / L${args.libr}`; 54 | } 55 | if (args.branch) { 56 | resourceUri += `&branch=${encodeURIComponent(args.branch)}`; 57 | resourceName += ` / Branch ${args.branch}`; 58 | } 59 | // Potentially add filter params to URI/Name if needed for uniqueness 60 | 61 | // Add response to resources 62 | addResource(resourceUri, { 63 | name: resourceName, 64 | mimeType: "application/json", 65 | text: JSON.stringify(data, null, 2) 66 | }); 67 | 68 | // Format the response for MCP 69 | return { 70 | content: [{ 71 | type: "text", 72 | text: JSON.stringify(data, null, 2) 73 | }] 74 | }; 75 | } catch (error: any) { 76 | let errorMessage = `Error accessing JPL Periodic Orbits API: ${error.message}`; 77 | if (error.response) { 78 | // Include more detail from the API response if available 79 | errorMessage += `\nStatus: ${error.response.status}\nData: ${JSON.stringify(error.response.data)}`; 80 | } 81 | return { 82 | content: [{ 83 | type: "text", 84 | text: errorMessage 85 | }], 86 | isError: true 87 | }; 88 | } 89 | } 90 | 91 | // Export default for dynamic imports in index.ts 92 | export default periodicOrbitsHandler; 93 | -------------------------------------------------------------------------------- /src/handlers/jpl/sbdb.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | // Schema for validating JPL Small-Body Database request parameters 6 | export const sbdbParamsSchema = z.object({ 7 | sstr: z.string().min(1), 8 | full_precision: z.boolean().optional().default(false), 9 | solution_epoch: z.string().optional(), 10 | orbit_class: z.boolean().optional().default(false), 11 | body_type: z.enum(['ast', 'com', 'all']).optional().default('all'), 12 | phys_par: z.boolean().optional().default(false), 13 | close_approach: z.boolean().optional().default(false), 14 | ca_time: z.enum(['all', 'now', 'fut', 'past']).optional().default('all'), 15 | ca_dist: z.enum(['au', 'ld', 'lu']).optional().default('au'), 16 | ca_tbl: z.enum(['elem', 'approach']).optional().default('approach'), 17 | format: z.enum(['json', 'xml']).optional().default('json') 18 | }); 19 | 20 | // Define the request parameter type based on the schema 21 | export type SbdbParams = z.infer; 22 | 23 | /** 24 | * Handle requests for JPL's Small-Body Database 25 | */ 26 | export async function jplSbdbHandler(params: SbdbParams) { 27 | try { 28 | const { 29 | sstr, 30 | full_precision, 31 | solution_epoch, 32 | orbit_class, 33 | body_type, 34 | phys_par, 35 | close_approach, 36 | ca_time, 37 | ca_dist, 38 | ca_tbl, 39 | format 40 | } = params; 41 | 42 | // Construct the SBDB query URL 43 | const url = 'https://ssd-api.jpl.nasa.gov/sbdb.api'; 44 | 45 | // Prepare the query parameters 46 | const queryParams: Record = { 47 | sstr 48 | }; 49 | 50 | // Add optional parameters 51 | if (full_precision) queryParams.full_precision = full_precision ? 'yes' : 'no'; 52 | if (solution_epoch) queryParams.solution_epoch = solution_epoch; 53 | if (orbit_class) queryParams.orbit_class = orbit_class ? 'yes' : 'no'; 54 | if (body_type !== 'all') queryParams.body_type = body_type; 55 | if (phys_par) queryParams.phys_par = phys_par ? 'yes' : 'no'; 56 | if (close_approach) queryParams.close_approach = close_approach ? 'yes' : 'no'; 57 | if (ca_time !== 'all') queryParams.ca_time = ca_time; 58 | if (ca_dist !== 'au') queryParams.ca_dist = ca_dist; 59 | if (ca_tbl !== 'approach') queryParams.ca_tbl = ca_tbl; 60 | if (format !== 'json') queryParams.format = format; 61 | 62 | // Transform parameter names from underscore to hyphenated format 63 | const transformedParams = transformParamsToHyphenated(queryParams); 64 | 65 | // Make the request to SBDB API 66 | const response = await axios.get(url, { params: transformedParams }); 67 | 68 | // Return the response 69 | return { 70 | content: [ 71 | { 72 | type: "text", 73 | text: `Retrieved data for small body "${params.sstr}".` 74 | }, 75 | { 76 | type: "text", 77 | text: JSON.stringify(response.data, null, 2) 78 | } 79 | ], 80 | isError: false 81 | }; 82 | } catch (error: any) { 83 | console.error('Error in JPL SBDB handler:', error); 84 | 85 | return { 86 | isError: true, 87 | content: [{ 88 | type: "text", 89 | text: `Error: ${error.message || 'An unexpected error occurred'}` 90 | }] 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/env-setup.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import dotenv from 'dotenv'; 4 | 5 | /** 6 | * Parse command line arguments for NASA API key 7 | * Looks for --nasa-api-key=value or --nasa-api-key value 8 | */ 9 | function parseCommandLineArgs() { 10 | const args = process.argv.slice(2); 11 | 12 | for (let i = 0; i < args.length; i++) { 13 | // Check for --nasa-api-key=value format 14 | if (args[i].startsWith('--nasa-api-key=')) { 15 | return args[i].split('=')[1]; 16 | } 17 | 18 | // Check for --nasa-api-key value format 19 | if (args[i] === '--nasa-api-key' && i + 1 < args.length) { 20 | return args[i + 1]; 21 | } 22 | } 23 | 24 | return null; 25 | } 26 | 27 | /** 28 | * Ensures that environment variables are properly loaded from .env files 29 | * This function will: 30 | * 1. Try to load from .env in current directory 31 | * 2. Try to load from .env in parent directory 32 | * 3. Try to load from .env in dist directory 33 | * 4. Copy the .env file to ensure it's available where needed 34 | * 5. Check for command line arguments 35 | */ 36 | export function setupEnvironment() { 37 | try { 38 | const currentDir = process.cwd(); 39 | const rootEnvPath = path.join(currentDir, '.env'); 40 | const distEnvPath = path.join(currentDir, 'dist', '.env'); 41 | 42 | // First try standard .env loading 43 | dotenv.config(); 44 | 45 | // If running from dist, also try parent directory 46 | if (currentDir.includes('dist')) { 47 | const parentEnvPath = path.join(currentDir, '..', '.env'); 48 | if (fs.existsSync(parentEnvPath)) { 49 | dotenv.config({ path: parentEnvPath }); 50 | } 51 | } 52 | 53 | // Also try explicit paths 54 | if (fs.existsSync(rootEnvPath)) { 55 | dotenv.config({ path: rootEnvPath }); 56 | } 57 | 58 | if (fs.existsSync(distEnvPath)) { 59 | dotenv.config({ path: distEnvPath }); 60 | } 61 | 62 | // Ensure dist directory has a copy of .env 63 | if (fs.existsSync(rootEnvPath) && !fs.existsSync(distEnvPath)) { 64 | try { 65 | // Create dist directory if it doesn't exist 66 | if (!fs.existsSync(path.join(currentDir, 'dist'))) { 67 | fs.mkdirSync(path.join(currentDir, 'dist'), { recursive: true }); 68 | } 69 | fs.copyFileSync(rootEnvPath, distEnvPath); 70 | } catch (error) { 71 | console.error('Error copying .env to dist directory:', error); 72 | // Continue despite error 73 | } 74 | } 75 | 76 | // Check for command line argument 77 | const cmdApiKey = parseCommandLineArgs(); 78 | if (cmdApiKey) { 79 | process.env.NASA_API_KEY = cmdApiKey; 80 | } 81 | // Explicitly set NASA_API_KEY from .env content if not already set 82 | else if (!process.env.NASA_API_KEY && fs.existsSync(rootEnvPath)) { 83 | try { 84 | const envContent = fs.readFileSync(rootEnvPath, 'utf8'); 85 | const match = envContent.match(/NASA_API_KEY=([^\n]+)/); 86 | if (match && match[1]) { 87 | process.env.NASA_API_KEY = match[1].trim(); 88 | } 89 | } catch (error) { 90 | console.error('Error reading .env file:', error); 91 | // Continue despite error 92 | } 93 | } 94 | } catch (error) { 95 | console.error('Error setting up environment:', error); 96 | // Continue despite error to allow server to try to start anyway 97 | } 98 | } 99 | 100 | // Export a default function for easy importing 101 | export default setupEnvironment; -------------------------------------------------------------------------------- /src/handlers/nasa/apod.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { nasaApiRequest } from '../../utils/api-client'; 3 | import { addResource } from '../../resources'; 4 | import axios from 'axios'; 5 | 6 | // Schema for validating APOD request parameters 7 | export const apodParamsSchema = z.object({ 8 | date: z.string().optional(), 9 | hd: z.boolean().optional(), 10 | count: z.number().int().positive().optional(), 11 | start_date: z.string().optional(), 12 | end_date: z.string().optional(), 13 | thumbs: z.boolean().optional() 14 | }); 15 | 16 | // Define the request parameter type based on the schema 17 | export type ApodParams = z.infer; 18 | 19 | /** 20 | * Handle requests for NASA's Astronomy Picture of the Day (APOD) API 21 | */ 22 | export async function nasaApodHandler(params: ApodParams) { 23 | try { 24 | // Call the NASA APOD API 25 | const result = await nasaApiRequest('/planetary/apod', params); 26 | 27 | // Store results as resources 28 | const processedResult = await processApodResultWithBase64(result); 29 | 30 | return { 31 | content: [ 32 | { 33 | type: "text", 34 | text: processedResult.summary 35 | }, 36 | ...processedResult.images.map(img => ({ 37 | type: "text", 38 | text: `![${img.title}](${img.url})` 39 | })), 40 | ...processedResult.images.map(img => ({ 41 | type: "image", 42 | data: img.base64, 43 | mimeType: img.mimeType || "image/jpeg" 44 | })) 45 | ], 46 | isError: false 47 | }; 48 | } catch (error: any) { 49 | console.error('Error in APOD handler:', error); 50 | 51 | return { 52 | content: [ 53 | { 54 | type: "text", 55 | text: `Error retrieving APOD data: ${error.message}` 56 | } 57 | ], 58 | isError: true 59 | }; 60 | } 61 | } 62 | 63 | // New async version that fetches and encodes images as base64 64 | async function processApodResultWithBase64(result: any) { 65 | const results = Array.isArray(result) ? result : [result]; 66 | let summary = ''; 67 | const images: any[] = []; 68 | for (const apod of results) { 69 | const apodId = `nasa://apod/image?date=${apod.date}`; 70 | let mimeType = 'image/jpeg'; 71 | if (apod.url) { 72 | if (apod.url.endsWith('.png')) mimeType = 'image/png'; 73 | else if (apod.url.endsWith('.gif')) mimeType = 'image/gif'; 74 | else if (apod.url.endsWith('.jpg') || apod.url.endsWith('.jpeg')) mimeType = 'image/jpeg'; 75 | } 76 | addResource(apodId, { 77 | name: `Astronomy Picture of the Day - ${apod.title}`, 78 | mimeType: 'application/json', 79 | text: JSON.stringify(apod, null, 2) 80 | }); 81 | summary += `## ${apod.title} (${apod.date})\n\n${apod.explanation}\n\n`; 82 | if (apod.url && (!apod.media_type || apod.media_type === 'image')) { 83 | summary += `Image URL: ${apod.url}\n\n`; 84 | let base64 = null; 85 | try { 86 | const imageResponse = await axios.get(apod.url, { responseType: 'arraybuffer', timeout: 30000 }); 87 | base64 = Buffer.from(imageResponse.data).toString('base64'); 88 | } catch (err) { 89 | console.error('Failed to fetch APOD image for base64:', apod.url, err); 90 | } 91 | images.push({ 92 | url: apod.url, 93 | title: apod.title, 94 | resourceUri: apodId, 95 | mimeType, 96 | base64 97 | }); 98 | } 99 | } 100 | return { 101 | summary, 102 | images 103 | }; 104 | } 105 | 106 | // Export the handler function directly as default 107 | export default nasaApodHandler; 108 | -------------------------------------------------------------------------------- /src/handlers/nasa/earth.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { nasaApiRequest } from '../../utils/api-client'; 3 | import { addResource } from '../../resources'; 4 | 5 | // Schema for validating Earth API request parameters 6 | export const earthParamsSchema = z.object({ 7 | lon: z.number().or(z.string().regex(/^-?\d+(\.\d+)?$/).transform(Number)), 8 | lat: z.number().or(z.string().regex(/^-?\d+(\.\d+)?$/).transform(Number)), 9 | date: z.string().optional(), 10 | dim: z.number().optional(), 11 | cloud_score: z.boolean().optional(), 12 | api_key: z.string().optional() 13 | }); 14 | 15 | // Define the request parameter type based on the schema 16 | export type EarthParams = z.infer; 17 | 18 | /** 19 | * Handle requests for NASA's Earth API (Landsat imagery) 20 | */ 21 | export async function nasaEarthHandler(params: EarthParams) { 22 | try { 23 | // Validate required parameters 24 | if (params.lon === undefined || params.lat === undefined) { 25 | return { 26 | content: [ 27 | { 28 | type: "text", 29 | text: "Error: Both longitude (lon) and latitude (lat) parameters are required" 30 | } 31 | ], 32 | isError: true 33 | }; 34 | } 35 | 36 | // Call the NASA Earth API (Landsat imagery) 37 | const result = await nasaApiRequest('/planetary/earth/imagery', params); 38 | 39 | // Check if we received an error response 40 | if (result.isError) { 41 | return result; 42 | } 43 | 44 | // Store results as resources and format response 45 | const processedResult = processEarthResult(result, params); 46 | 47 | return { 48 | content: [ 49 | { 50 | type: "text", 51 | text: processedResult.summary 52 | }, 53 | // Include image URL 54 | { 55 | type: "text", 56 | text: `![Landsat imagery at coordinates (${params.lon}, ${params.lat})](${processedResult.imageUrl})` 57 | } 58 | ], 59 | isError: false 60 | }; 61 | } catch (error: any) { 62 | console.error('Error in Earth API handler:', error); 63 | 64 | return { 65 | content: [ 66 | { 67 | type: "text", 68 | text: `Error retrieving Earth imagery data: ${error.message}` 69 | } 70 | ], 71 | isError: true 72 | }; 73 | } 74 | } 75 | 76 | /** 77 | * Process Earth API result 78 | * Convert to resource and return formatted data 79 | */ 80 | function processEarthResult(result: any, params: EarthParams) { 81 | // Create a unique ID for this Earth imagery entry 82 | const earthId = `nasa://earth/imagery?lon=${params.lon}&lat=${params.lat}${params.date ? `&date=${params.date}` : ''}`; 83 | 84 | // Extract image URL 85 | const imageUrl = result.url || ''; 86 | 87 | // Store as a resource 88 | addResource(earthId, { 89 | name: `Landsat imagery at coordinates (${params.lon}, ${params.lat})`, 90 | mimeType: 'application/json', 91 | text: JSON.stringify({ 92 | ...result, 93 | coordinates: { 94 | lon: params.lon, 95 | lat: params.lat 96 | }, 97 | date: params.date || 'latest', 98 | image_url: imageUrl 99 | }, null, 2) 100 | }); 101 | 102 | // Create summary text 103 | const summary = `## Landsat Satellite Imagery\n\n` + 104 | `**Location**: Longitude ${params.lon}, Latitude ${params.lat}\n` + 105 | `**Date**: ${result.date || params.date || 'Latest available'}\n` + 106 | `**Image ID**: ${result.id || 'Not provided'}\n\n` + 107 | `**Image URL**: ${imageUrl}\n\n`; 108 | 109 | return { 110 | summary, 111 | imageUrl 112 | }; 113 | } 114 | 115 | // Export the handler function directly as default 116 | export default nasaEarthHandler; 117 | -------------------------------------------------------------------------------- /src/handlers/nasa/gibs.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { addResource } from '../../resources'; 4 | 5 | // Schema for validating GIBS request parameters 6 | export const gibsParamsSchema = z.object({ 7 | date: z.string().optional(), 8 | layer: z.string(), 9 | resolution: z.number().optional(), 10 | format: z.enum(['png', 'jpg', 'jpeg']).optional().default('png'), 11 | bbox: z.string().optional() 12 | }); 13 | 14 | // Define the request parameter type based on the schema 15 | export type GibsParams = z.infer; 16 | 17 | /** 18 | * Handle requests for NASA's Global Imagery Browse Services (GIBS) API 19 | */ 20 | export async function nasaGibsHandler(params: GibsParams) { 21 | try { 22 | const { date, layer, resolution, format, bbox } = params; 23 | 24 | // Default bbox if not provided 25 | const bboxParam = bbox || '-180,-90,180,90'; 26 | 27 | // Construct the GIBS URL 28 | const baseUrl = 'https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'; 29 | 30 | // Convert format to proper MIME type format for WMS 31 | const mimeFormat = format === 'jpg' ? 'jpeg' : format; 32 | 33 | const requestParams = { 34 | SERVICE: 'WMS', 35 | VERSION: '1.3.0', 36 | REQUEST: 'GetMap', 37 | FORMAT: `image/${mimeFormat}`, 38 | LAYERS: layer, 39 | CRS: 'EPSG:4326', 40 | BBOX: bboxParam, 41 | WIDTH: 720, 42 | HEIGHT: 360, 43 | TIME: date 44 | }; 45 | 46 | // Make the request to GIBS directly 47 | const response = await axios({ 48 | url: baseUrl, 49 | params: requestParams, 50 | responseType: 'arraybuffer', 51 | timeout: 30000 52 | }); 53 | 54 | // Convert response to base64 55 | const imageBase64 = Buffer.from(response.data).toString('base64'); 56 | 57 | // Register the image as a resource 58 | const formattedDate = date || new Date().toISOString().split('T')[0]; 59 | const resourceUri = `nasa://gibs/imagery?layer=${layer}&date=${formattedDate}`; 60 | 61 | addResource(resourceUri, { 62 | name: `NASA GIBS: ${layer} (${formattedDate})`, 63 | mimeType: `image/${format}`, 64 | // Store metadata as text (optional) 65 | text: JSON.stringify({ 66 | layer: layer, 67 | date: formattedDate, 68 | bbox: bboxParam, 69 | width: 720, 70 | height: 360 71 | }), 72 | // Store the actual image data as a blob 73 | blob: Buffer.from(response.data) 74 | }); 75 | 76 | // Return metadata and image data 77 | return { 78 | content: [ 79 | { 80 | type: "text", 81 | text: `NASA GIBS satellite imagery for ${layer} on ${date || 'latest'}` 82 | }, 83 | { 84 | type: "image", 85 | mimeType: `image/${format}`, 86 | data: imageBase64 87 | }, 88 | { 89 | type: "text", 90 | text: `Resource registered at: ${resourceUri}` 91 | } 92 | ], 93 | isError: false 94 | }; 95 | } catch (error: any) { 96 | console.error('Error in GIBS handler:', error); 97 | 98 | if (error.name === 'ZodError') { 99 | return { 100 | content: [ 101 | { 102 | type: "text", 103 | text: `Invalid request parameters: ${error.message}` 104 | } 105 | ], 106 | isError: true 107 | }; 108 | } 109 | 110 | return { 111 | content: [ 112 | { 113 | type: "text", 114 | text: `Error retrieving GIBS data: ${error.message}` 115 | } 116 | ], 117 | isError: true 118 | }; 119 | } 120 | } 121 | 122 | // Add a default export for the handler 123 | export default nasaGibsHandler; 124 | -------------------------------------------------------------------------------- /src/handlers/jpl/horizons_file.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import FormData from 'form-data'; // Need form-data for multipart POST 3 | import { addResource } from '../../resources'; 4 | 5 | /** 6 | * Handler for JPL Horizons File API (POST request) 7 | * 8 | * This API provides ephemeris data for solar system objects using a file-based input. 9 | * It accepts the same parameters as the GET version but formats them for file submission. 10 | * 11 | * @param args Request parameters (e.g., COMMAND, START_TIME, STOP_TIME, etc.) 12 | * @returns API response 13 | */ 14 | export async function horizonsFileHandler(args: Record) { 15 | try { 16 | // Base URL for the Horizons File API (POST) 17 | const baseUrl = 'https://ssd.jpl.nasa.gov/api/horizons_file.api'; 18 | 19 | // Format arguments into the key='value' text format for the input file 20 | // DO NOT include format here, it's a separate form field. 21 | const formattedArgs = { ...args }; 22 | delete formattedArgs.format; // Remove format if present 23 | 24 | let fileContent = '!$$SOF\n'; // Add !SOF marker 25 | for (const [key, value] of Object.entries(formattedArgs)) { 26 | let formattedValue: string | number; 27 | const upperKey = key.toUpperCase(); 28 | 29 | // Leave numbers unquoted 30 | if (typeof value === 'number') { 31 | formattedValue = value; 32 | } 33 | // Quote ALL other values (strings, including YES/NO) 34 | else { 35 | formattedValue = `'${String(value).replace(/'/g, "\'")}'`; 36 | } 37 | 38 | fileContent += `${upperKey}=${formattedValue}\n`; 39 | } 40 | fileContent += '!$$EOF\n'; // Correct !EOF marker 41 | 42 | // Create FormData payload 43 | const form = new FormData(); 44 | // Add format as a separate field 45 | form.append('format', args.format || 'json'); 46 | // Add the file content under the 'input' field name 47 | form.append('input', fileContent, { 48 | filename: 'horizons_input.txt', // Required filename, content doesn't matter 49 | contentType: 'text/plain', 50 | }); 51 | 52 | // Make the API request using POST with multipart/form-data 53 | const response = await axios.post(baseUrl, form, { 54 | headers: { 55 | ...form.getHeaders(), // Important for correct boundary 56 | }, 57 | }); 58 | const data = response.data; // Assume response is JSON based on 'format=json' 59 | 60 | // Create a resource URI that represents this query (similar to GET handler) 61 | let resourceUri = 'jpl://horizons-file'; // Distinguish from GET 62 | let resourceName = 'JPL Horizons file-based ephemeris data'; 63 | 64 | if (args.COMMAND) { 65 | resourceUri += `/object/${encodeURIComponent(args.COMMAND)}`; 66 | resourceName = `${args.COMMAND} ephemeris data (file input)`; 67 | if (args.START_TIME && args.STOP_TIME) { 68 | resourceName += ` (${args.START_TIME} to ${args.STOP_TIME})`; 69 | } 70 | } 71 | 72 | // Add response to resources 73 | addResource(resourceUri, { 74 | name: resourceName, 75 | mimeType: "application/json", // Assuming JSON response 76 | text: JSON.stringify(data, null, 2) 77 | }); 78 | 79 | // Format the response 80 | return { 81 | content: [{ 82 | type: "text", 83 | text: JSON.stringify(data, null, 2) 84 | }] 85 | }; 86 | } catch (error: any) { 87 | let errorMessage = `Error accessing JPL Horizons File API: ${error.message}`; 88 | if (error.response) { 89 | // Include more detail from the API response if available 90 | errorMessage += `\nStatus: ${error.response.status}\nData: ${JSON.stringify(error.response.data)}`; 91 | } 92 | return { 93 | content: [{ 94 | type: "text", 95 | text: errorMessage 96 | }], 97 | isError: true 98 | }; 99 | } 100 | } 101 | 102 | // Export default for dynamic imports in index.ts 103 | export default horizonsFileHandler; 104 | -------------------------------------------------------------------------------- /src/handlers/nasa/exoplanet.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { addResource } from '../../resources'; 4 | 5 | // Base URL for NASA's Exoplanet Archive 6 | const EXOPLANET_API_URL = 'https://exoplanetarchive.ipac.caltech.edu/cgi-bin/nstedAPI/nph-nstedAPI'; 7 | 8 | // Schema for validating Exoplanet Archive request parameters 9 | export const exoplanetParamsSchema = z.object({ 10 | table: z.string(), 11 | select: z.string().optional(), 12 | where: z.string().optional(), 13 | order: z.string().optional(), 14 | format: z.enum(['json', 'csv', 'ipac', 'xml']).optional().default('json'), 15 | limit: z.number().int().min(1).max(1000).optional() 16 | }); 17 | 18 | // Define the request parameter type based on the schema 19 | export type ExoplanetParams = z.infer; 20 | 21 | /** 22 | * Handle requests for NASA's Exoplanet Archive 23 | */ 24 | export async function nasaExoplanetHandler(params: ExoplanetParams) { 25 | try { 26 | const { table, select, where, order, format, limit } = params; 27 | 28 | // Construct the API parameters directly - nstedAPI has different params than TAP/sync 29 | const apiParams: Record = { 30 | table: table, 31 | format: format 32 | }; 33 | 34 | // Add optional parameters if provided 35 | if (select) { 36 | apiParams.select = select; 37 | } 38 | 39 | if (where) { 40 | apiParams.where = where; 41 | } 42 | 43 | if (order) { 44 | apiParams.order = order; 45 | } 46 | 47 | if (limit) { 48 | apiParams.top = limit; // Use 'top' instead of 'limit' for this API 49 | } 50 | 51 | // Make the request to the Exoplanet Archive 52 | const response = await axios.get(EXOPLANET_API_URL, { 53 | params: apiParams 54 | }); 55 | 56 | // Create a resource ID based on the query parameters 57 | const resourceId = `nasa://exoplanet/data?table=${table}${where ? `&where=${encodeURIComponent(where)}` : ''}${limit ? `&limit=${limit}` : ''}`; 58 | 59 | // Register the response as a resource 60 | addResource(resourceId, { 61 | name: `Exoplanet data from ${table}${where ? ` with filter` : ''}`, 62 | mimeType: format === 'json' ? 'application/json' : 'text/plain', 63 | text: format === 'json' ? JSON.stringify(response.data, null, 2) : response.data 64 | }); 65 | 66 | // Format response based on the data type 67 | if (Array.isArray(response.data) && response.data.length > 0) { 68 | // If we got an array of results 69 | const count = response.data.length; 70 | return { 71 | content: [ 72 | { 73 | type: "text", 74 | text: `Found ${count} exoplanet records from the ${table} table.` 75 | }, 76 | { 77 | type: "text", 78 | text: JSON.stringify(response.data.slice(0, 10), null, 2) + 79 | (count > 10 ? `\n... and ${count - 10} more records` : '') 80 | } 81 | ], 82 | isError: false 83 | }; 84 | } else { 85 | // If we got a different format or empty results 86 | return { 87 | content: [ 88 | { 89 | type: "text", 90 | text: `Exoplanet query complete. Results from ${table} table.` 91 | }, 92 | { 93 | type: "text", 94 | text: typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2) 95 | } 96 | ], 97 | isError: false 98 | }; 99 | } 100 | } catch (error: any) { 101 | console.error('Error in Exoplanet handler:', error); 102 | 103 | return { 104 | isError: true, 105 | content: [{ 106 | type: "text", 107 | text: `Error accessing NASA Exoplanet Archive: ${error.message || 'Unknown error'}` 108 | }] 109 | }; 110 | } 111 | } 112 | 113 | // Export the handler function directly as default 114 | export default nasaExoplanetHandler; 115 | -------------------------------------------------------------------------------- /src/handlers/jpl/sentry.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { addResource } from '../../resources'; 3 | import { transformParamsToHyphenated } from '../../utils/param-transformer'; 4 | 5 | /** 6 | * Handler for JPL Sentry API 7 | * 8 | * This API provides NEO Earth impact risk assessment data for potentially hazardous asteroids. 9 | * 10 | * @param args Request parameters 11 | * @returns API response 12 | */ 13 | export async function sentryHandler(args: Record) { 14 | try { 15 | // Base URL for the Sentry API 16 | const baseUrl = 'https://ssd-api.jpl.nasa.gov/sentry.api'; 17 | 18 | // Transform parameter names from underscore to hyphenated format 19 | const transformedParams = transformParamsToHyphenated(args); 20 | 21 | // Make the API request 22 | const response = await axios.get(baseUrl, { params: transformedParams }); 23 | const data = response.data; 24 | 25 | // Create a resource URI that represents this query 26 | let resourceUri: string; 27 | let resourceName: string; 28 | 29 | if (args.des) { 30 | // Object mode - query for a specific object 31 | resourceUri = `jpl://sentry/object/${args.des}`; 32 | resourceName = `Impact risk assessment for object ${args.des}`; 33 | 34 | // Check if object is in the Sentry database or was removed 35 | if (data.error && data.error === "specified object removed") { 36 | resourceName += ` (removed on ${data.removed})`; 37 | } else if (data.error && data.error === "specified object not found") { 38 | resourceName += ` (not found)`; 39 | } else if (data.summary) { 40 | resourceName = `Impact risk assessment for ${data.summary.fullname}`; 41 | } 42 | } else if (args.spk) { 43 | // Object mode - query for a specific object by SPK-ID 44 | resourceUri = `jpl://sentry/object/${args.spk}`; 45 | resourceName = `Impact risk assessment for object SPK ${args.spk}`; 46 | 47 | // Update name if we have more info 48 | if (data.summary) { 49 | resourceName = `Impact risk assessment for ${data.summary.fullname}`; 50 | } 51 | } else if (args.removed === true || args.removed === '1' || args.removed === 'Y' || args.removed === 'true') { 52 | // Removed objects mode 53 | resourceUri = `jpl://sentry/removed`; 54 | resourceName = `Objects removed from Sentry impact monitoring`; 55 | } else if (args.all === true || args.all === '1' || args.all === 'Y' || args.all === 'true') { 56 | // Virtual impactors mode 57 | resourceUri = `jpl://sentry/vi`; 58 | 59 | // Add any constraints to the URI 60 | const constraints = Object.entries(args) 61 | .filter(([key]) => key !== 'all') 62 | .map(([key, value]) => `${key}=${value}`) 63 | .join('&'); 64 | 65 | if (constraints) { 66 | resourceUri += `?${constraints}`; 67 | } 68 | 69 | resourceName = `Sentry virtual impactors data`; 70 | } else { 71 | // Summary mode 72 | resourceUri = `jpl://sentry/summary`; 73 | 74 | // Add any constraints to the URI 75 | const constraints = Object.entries(args) 76 | .map(([key, value]) => `${key}=${value}`) 77 | .join('&'); 78 | 79 | if (constraints) { 80 | resourceUri += `?${constraints}`; 81 | } 82 | 83 | resourceName = `Sentry impact risk summary data`; 84 | } 85 | 86 | // Add response to resources 87 | addResource(resourceUri, { 88 | name: resourceName, 89 | mimeType: "application/json", 90 | text: JSON.stringify(data, null, 2) 91 | }); 92 | 93 | // Format the response 94 | return { 95 | content: [{ 96 | type: "text", 97 | text: JSON.stringify(data, null, 2) 98 | }] 99 | }; 100 | } catch (error: any) { 101 | return { 102 | content: [{ 103 | type: "text", 104 | text: `Error accessing JPL Sentry API: ${error.message}` 105 | }], 106 | isError: true 107 | }; 108 | } 109 | } 110 | 111 | // Export default for dynamic imports 112 | export default sentryHandler; 113 | -------------------------------------------------------------------------------- /src/handlers/nasa/firms.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { addResource } from '../../resources'; 4 | 5 | const FIRMS_API_BASE_URL = 'https://firms.modaps.eosdis.nasa.gov/api/area/csv'; 6 | 7 | // Schema for validating FIRMS request parameters 8 | export const firmsParamsSchema = z.object({ 9 | latitude: z.number(), 10 | longitude: z.number(), 11 | radius: z.number().optional().default(1.0), 12 | days: z.number().int().min(1).max(10).optional().default(1), 13 | source: z.enum(['VIIRS_SNPP_NRT', 'MODIS_NRT', 'VIIRS_NOAA20_NRT']).optional().default('VIIRS_SNPP_NRT') 14 | }); 15 | 16 | // Define the request parameter type based on the schema 17 | export type FirmsParams = z.infer; 18 | 19 | /** 20 | * Handle requests for NASA's FIRMS (Fire Information for Resource Management System) API 21 | */ 22 | export async function nasaFirmsHandler(params: FirmsParams) { 23 | try { 24 | const { latitude, longitude, radius, days, source } = params; 25 | 26 | // Validate required parameters 27 | if (!process.env.NASA_API_KEY) { 28 | return { 29 | isError: true, 30 | content: [{ 31 | type: "text", 32 | text: "Error: NASA API key is required for FIRMS requests" 33 | }] 34 | }; 35 | } 36 | 37 | // Get the NASA API key from environment variables 38 | const apiKey = process.env.NASA_API_KEY; 39 | 40 | // Construct request URL 41 | const url = FIRMS_API_BASE_URL; 42 | 43 | // Send request to FIRMS API 44 | const response = await axios.get(url, { 45 | params: { 46 | lat: latitude, 47 | lon: longitude, 48 | radius: radius, 49 | days: days, 50 | source: source, 51 | api_key: apiKey 52 | } 53 | }); 54 | 55 | // Parse the CSV response into a structured format 56 | const csvData = response.data; 57 | const rows = csvData.split('\n'); 58 | 59 | if (rows.length < 2) { 60 | return { results: [] }; 61 | } 62 | 63 | const headers = rows[0].split(','); 64 | const results = rows.slice(1) 65 | .filter((row: string) => row.trim() !== '') 66 | .map((row: string) => { 67 | const values = row.split(','); 68 | const entry: Record = {}; 69 | 70 | headers.forEach((header: string, index: number) => { 71 | const value = values[index] ? values[index].trim() : ''; 72 | // Try to convert numeric values 73 | const numValue = Number(value); 74 | entry[header] = !isNaN(numValue) && value !== '' ? numValue : value; 75 | }); 76 | 77 | return entry; 78 | }); 79 | 80 | // Register the response as a resource 81 | const resourceId = `nasa://firms/data?lat=${latitude}&lon=${longitude}&days=${days}&source=${source}`; 82 | const resourceData = { 83 | metadata: { 84 | latitude, 85 | longitude, 86 | radius, 87 | days, 88 | source 89 | }, 90 | results 91 | }; 92 | 93 | addResource(resourceId, { 94 | name: `Fire Data near (${latitude}, ${longitude}) for the past ${days} day(s)`, 95 | mimeType: 'application/json', 96 | text: JSON.stringify(resourceData, null, 2) 97 | }); 98 | 99 | // Return data in MCP format 100 | return { 101 | content: [ 102 | { 103 | type: "text", 104 | text: `Found ${results.length} fire hotspots near (${latitude}, ${longitude}) in the past ${days} day(s)` 105 | }, 106 | { 107 | type: "text", 108 | text: JSON.stringify(results, null, 2) 109 | } 110 | ], 111 | isError: false 112 | }; 113 | } catch (error: any) { 114 | console.error('Error in FIRMS handler:', error); 115 | 116 | return { 117 | isError: true, 118 | content: [{ 119 | type: "text", 120 | text: `Error: ${error.message || 'An unexpected error occurred'}` 121 | }] 122 | }; 123 | } 124 | } 125 | 126 | // Export the handler function directly as default 127 | export default nasaFirmsHandler; 128 | -------------------------------------------------------------------------------- /src/handlers/nasa/eonet.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { nasaApiRequest } from '../../utils/api-client'; 4 | import { EonetParams } from '../setup'; 5 | import { addResource } from '../../resources'; 6 | 7 | // Define the EONET API base URL 8 | const EONET_API_BASE_URL = 'https://eonet.gsfc.nasa.gov/api'; 9 | 10 | /** 11 | * Handle requests for NASA's Earth Observatory Natural Event Tracker (EONET) API 12 | */ 13 | export async function nasaEonetHandler(params: EonetParams) { 14 | try { 15 | const { category, days, source, status, limit } = params; 16 | 17 | // Build the endpoint path 18 | let endpointPath = '/v3/events'; 19 | const apiParams: Record = {}; 20 | 21 | // Add query parameters - using more default values to ensure we get results 22 | if (days) apiParams.days = days; 23 | if (source) apiParams.source = source; 24 | if (status) apiParams.status = status; 25 | if (limit) apiParams.limit = limit; 26 | 27 | // If no status is provided, default to "all" to ensure we get some events 28 | if (!status) apiParams.status = "all"; 29 | 30 | // If no days parameter, default to 60 days to ensure we get more events 31 | if (!days) apiParams.days = 60; 32 | 33 | // If a category is specified, use the category-specific endpoint 34 | if (category) { 35 | endpointPath = `/v3/categories/${category}`; 36 | } 37 | 38 | // Use direct axios call with the EONET-specific base URL 39 | const response = await axios.get(`${EONET_API_BASE_URL}${endpointPath}`, { 40 | params: apiParams, 41 | timeout: 10000 // 10 second timeout 42 | }); 43 | 44 | // If we don't have any events, try again with broader parameters 45 | if (!response.data.events || response.data.events.length === 0) { 46 | // Reset to the main events endpoint for maximum results 47 | endpointPath = '/v3/events'; 48 | 49 | // Use broader parameters 50 | const broadParams = { 51 | status: 'all', // Get both open and closed events 52 | days: 90, // Look back further 53 | limit: limit || 50 // Increase the limit 54 | }; 55 | 56 | const broadResponse = await axios.get(`${EONET_API_BASE_URL}${endpointPath}`, { 57 | params: broadParams, 58 | timeout: 10000 59 | }); 60 | 61 | // Register the response as a resource 62 | const resourceId = `nasa://eonet/events?days=${broadParams.days}&status=${broadParams.status}`; 63 | addResource(resourceId, { 64 | name: `EONET Events (${broadParams.days} days, ${broadParams.status} status)`, 65 | mimeType: 'application/json', 66 | text: JSON.stringify(broadResponse.data, null, 2) 67 | }); 68 | 69 | return { 70 | content: [{ 71 | type: "text", 72 | text: `Used broader search criteria due to no events found with original parameters. Found ${broadResponse.data.events?.length || 0} events.` 73 | }], 74 | isError: false 75 | }; 76 | } 77 | 78 | // Register the response as a resource 79 | const resourceParams = []; 80 | if (days) resourceParams.push(`days=${days}`); 81 | if (category) resourceParams.push(`category=${category}`); 82 | if (status) resourceParams.push(`status=${status}`); 83 | 84 | const resourceId = `nasa://eonet/events${category ? '/categories/' + category : ''}?${resourceParams.join('&')}`; 85 | addResource(resourceId, { 86 | name: `EONET Events${category ? ' (' + category + ')' : ''}`, 87 | mimeType: 'application/json', 88 | text: JSON.stringify(response.data, null, 2) 89 | }); 90 | 91 | // Return the original result 92 | return { 93 | content: [{ 94 | type: "text", 95 | text: `Found ${response.data.events?.length || 0} EONET events.` 96 | }], 97 | isError: false 98 | }; 99 | } catch (error: any) { 100 | console.error('Error in EONET handler:', error); 101 | 102 | return { 103 | isError: true, 104 | content: [{ 105 | type: "text", 106 | text: `Error: ${error.message || 'An unexpected error occurred'}` 107 | }] 108 | }; 109 | } 110 | } 111 | 112 | // Export the handler function directly as default 113 | export default nasaEonetHandler; 114 | -------------------------------------------------------------------------------- /src/tests/direct-api-test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import dotenv from 'dotenv'; 3 | 4 | // Load environment variables 5 | dotenv.config(); 6 | 7 | // Get API key 8 | const NASA_API_KEY = process.env.NASA_API_KEY || 'DEMO_KEY'; 9 | 10 | // Track failed API calls 11 | const failedCalls: string[] = []; 12 | 13 | /** 14 | * Test function for direct NASA API calls 15 | */ 16 | async function testNasaEndpoint(name: string, url: string, params: Record = {}, requiresApiKey: boolean = true) { 17 | try { 18 | console.log(`Testing ${name}...`); 19 | 20 | // Add API key to parameters if required 21 | const queryParams = { ...params }; 22 | if (requiresApiKey) { 23 | queryParams.api_key = NASA_API_KEY; 24 | } 25 | 26 | // Make direct request to NASA API 27 | const response = await axios.get(url, { params: queryParams }); 28 | 29 | console.log(`✅ ${name} API call successful`); 30 | return response.data; 31 | } catch (error: any) { 32 | console.error(`❌ Error in ${name}: ${error.message}`); 33 | if (error.response) { 34 | console.error(`Status: ${error.response.status}`); 35 | console.error(`Data:`, error.response.data); 36 | } 37 | // Add to failed calls list 38 | failedCalls.push(name); 39 | return null; 40 | } 41 | } 42 | 43 | /** 44 | * Run tests for all NASA APIs 45 | */ 46 | async function runDirectTests() { 47 | console.log("Starting direct NASA API tests...\n"); 48 | 49 | // Test APOD API 50 | await testNasaEndpoint( 51 | "NASA APOD", 52 | "https://api.nasa.gov/planetary/apod" 53 | ); 54 | 55 | // Test EPIC API 56 | await testNasaEndpoint( 57 | "NASA EPIC", 58 | "https://epic.gsfc.nasa.gov/api/natural/images", 59 | {}, 60 | false // EPIC API doesn't use API key in the URL 61 | ); 62 | 63 | // Test NEO API 64 | await testNasaEndpoint( 65 | "NASA NEO", 66 | "https://api.nasa.gov/neo/rest/v1/feed", 67 | { start_date: "2023-01-01", end_date: "2023-01-07" } 68 | ); 69 | 70 | // Test EONET API 71 | await testNasaEndpoint( 72 | "NASA EONET", 73 | "https://eonet.gsfc.nasa.gov/api/v3/events", 74 | { limit: 5 }, 75 | false // EONET doesn't require an API key 76 | ); 77 | 78 | // Test Mars Rover API 79 | await testNasaEndpoint( 80 | "Mars Rover", 81 | "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos", 82 | { sol: 1000, page: 1 } 83 | ); 84 | 85 | // Test GIBS API (this is a tile service that returns images) 86 | await testNasaEndpoint( 87 | "NASA GIBS", 88 | "https://gibs.earthdata.nasa.gov/wmts/epsg4326/best/MODIS_Terra_CorrectedReflectance_TrueColor/default/2023-01-01/250m/6/13/12.jpg", 89 | {}, 90 | false // GIBS doesn't require an API key 91 | ); 92 | 93 | // Test CMR API 94 | await testNasaEndpoint( 95 | "NASA CMR", 96 | "https://cmr.earthdata.nasa.gov/search/collections.json", 97 | { keyword: "temperature", page_size: 5 }, 98 | false // CMR doesn't use the api_key parameter 99 | ); 100 | 101 | // Test FIRMS API 102 | await testNasaEndpoint( 103 | "NASA FIRMS", 104 | "https://firms.modaps.eosdis.nasa.gov/api/area/csv/d67d279bd65f48d0602c9a9cff39fee9", // Token in URL 105 | { lat: 37.454, lon: -122.181, radius: 1.0 }, 106 | false // FIRMS uses token in URL, not as a parameter 107 | ); 108 | 109 | // Test NASA Image Library API 110 | await testNasaEndpoint( 111 | "NASA Images", 112 | "https://images-api.nasa.gov/search", 113 | { q: "moon", media_type: "image" }, 114 | false // Images API doesn't use the api_key parameter 115 | ); 116 | 117 | // Test Exoplanet Archive API (doesn't require API key) 118 | await testNasaEndpoint( 119 | "Exoplanet Archive", 120 | "https://exoplanetarchive.ipac.caltech.edu/TAP/sync", 121 | { 122 | query: "SELECT pl_name FROM ps WHERE rownum < 10", 123 | format: "json" 124 | }, 125 | false // Exoplanet Archive doesn't use an API key 126 | ); 127 | 128 | // Test JPL SBDB API (doesn't require API key) 129 | await testNasaEndpoint( 130 | "JPL SBDB", 131 | "https://ssd-api.jpl.nasa.gov/sbdb.api", 132 | { sstr: "Ceres" }, 133 | false // SBDB doesn't use the api_key parameter 134 | ); 135 | 136 | // Test JPL Fireball API (doesn't require API key) 137 | await testNasaEndpoint( 138 | "JPL Fireball", 139 | "https://ssd-api.jpl.nasa.gov/fireball.api", 140 | { limit: 5 }, 141 | false // Fireball API doesn't use the api_key parameter 142 | ); 143 | 144 | console.log("\nAll direct API tests completed!"); 145 | 146 | // Print summary of failed calls 147 | if (failedCalls.length > 0) { 148 | console.log("\n❌ Failed API calls:"); 149 | failedCalls.forEach(name => console.log(`- ${name}`)); 150 | } else { 151 | console.log("\n✅ All API calls were successful!"); 152 | } 153 | } 154 | 155 | // Run the tests 156 | runDirectTests().catch(err => { 157 | console.error("Unhandled error:", err); 158 | }); -------------------------------------------------------------------------------- /src/handlers/jpl/scout.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { jplApiRequest } from '../../utils/api-client'; 3 | import { ScoutParams } from '../setup'; 4 | import { addResource } from '../../resources'; // Import addResource 5 | 6 | /** 7 | * Process the Scout API result and format it for display. 8 | */ 9 | function processScoutResult(data: any, params: ScoutParams): string { 10 | // Handle API-level errors first 11 | if (data.error_code) { 12 | return `Error from Scout API (${data.error_code}): ${data.error_msg}`; 13 | } 14 | if (data.error) { // Handle other error format like "object does not exist" 15 | return `Error from Scout API: ${data.error}`; 16 | } 17 | 18 | let summary = `## JPL Scout Data\n\n`; 19 | 20 | if (params.tdes || params.orbit_id) { 21 | // Single object result 22 | if (!data.data || data.data.length === 0) { 23 | // If no specific error was returned, but data is empty, state that. 24 | return summary + `No data array found for object specified by ${params.tdes ? 'tdes='+params.tdes : ''}${params.orbit_id ? 'orbit_id='+params.orbit_id : ''}. Object may not exist or data type not available.`; 25 | } 26 | const obj = data.data[0]; 27 | summary += `**Object:** ${obj.neo || 'N/A'} (${params.tdes || params.orbit_id})\n`; 28 | summary += `**Last Observed:** ${obj.last_obs || 'N/A'}\n`; 29 | summary += `**Observations:** ${obj.n_obs || 'N/A'}\n`; 30 | summary += `**RMS:** ${obj.rms || 'N/A'}\n`; 31 | summary += `**MOID (AU):** ${obj.moid || 'N/A'}\n`; 32 | summary += `**V_inf (km/s):** ${obj.v_inf || 'N/A'}\n`; 33 | summary += `**Impact Probability:** ${obj.impact_prob || 'N/A'}\n`; 34 | if (obj.summary) { 35 | summary += `**Summary:** ${obj.summary}\n`; 36 | } 37 | // Add more fields if file=ephem, obs, crit, all are requested 38 | } else { 39 | // List result 40 | summary += `**Total Objects Found:** ${data.total || 'N/A'}\n`; 41 | summary += `**Showing:** ${data.count !== undefined ? data.count : 'N/A'} (Limit: ${params.limit || data.limit || 'default'})\n\n`; 42 | if (data.data && Array.isArray(data.data) && data.data.length > 0) { 43 | data.data.forEach((obj: any, index: number) => { 44 | summary += `### ${index + 1}. ${obj.neo || 'Unknown Designation'}\n`; 45 | summary += ` - Last Observed: ${obj.last_obs || 'N/A'}\n`; 46 | summary += ` - Observations: ${obj.n_obs || 'N/A'}\n`; 47 | summary += ` - MOID (AU): ${obj.moid || 'N/A'}\n`; 48 | summary += ` - Impact Probability: ${obj.impact_prob || 'N/A'}\n`; 49 | }); 50 | } else { 51 | summary += `No objects found matching criteria or returned in list.\n`; 52 | } 53 | } 54 | 55 | return summary; 56 | } 57 | 58 | /** 59 | * Handle requests for JPL's Scout API 60 | * Scout is a hazard assessment system that automatically calculates the potential 61 | * for an object to be an impactor based on the available observations. 62 | */ 63 | export async function jplScoutHandler(params: ScoutParams) { 64 | try { 65 | // Call the Scout API using jplApiRequest 66 | const result = await jplApiRequest('/scout.api', params); 67 | 68 | // Check for errors returned by jplApiRequest itself (network, etc.) 69 | if (result.isError) { 70 | return result; // Already formatted error response 71 | } 72 | 73 | // Check for API-specific errors within the payload (different formats) 74 | if (result.error_code || result.error) { 75 | return { 76 | isError: true, 77 | content: [{ 78 | type: "text", 79 | text: `Error from Scout API: ${result.error_msg || result.error}` 80 | }] 81 | }; 82 | } 83 | 84 | // Process the successful result 85 | const summaryText = processScoutResult(result, params); 86 | 87 | // Add the result as an MCP resource 88 | let resourceUri = 'jpl://scout/list'; 89 | if (params.tdes) { 90 | resourceUri = `jpl://scout?tdes=${params.tdes}`; 91 | } else if (params.orbit_id) { 92 | resourceUri = `jpl://scout?orbit_id=${params.orbit_id}`; 93 | } else if (params.limit) { 94 | resourceUri = `jpl://scout/list?limit=${params.limit}`; 95 | } // Add more specific URIs if other params like 'file' are used 96 | 97 | addResource(resourceUri, { 98 | name: `JPL Scout Data ${params.tdes || params.orbit_id || '(List)'}`, 99 | mimeType: 'application/json', 100 | text: JSON.stringify(result, null, 2) 101 | }); 102 | 103 | return { 104 | content: [{ 105 | type: "text", 106 | text: summaryText 107 | }], 108 | isError: false 109 | }; 110 | 111 | } catch (error: any) { // Catch unexpected errors during processing 112 | console.error('Error in JPL Scout handler:', error); 113 | return { 114 | isError: true, 115 | content: [{ 116 | type: "text", 117 | text: `Handler Error: ${error.message || 'An unexpected error occurred processing Scout data'}` 118 | }] 119 | }; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/handlers/nasa/images.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { addResource } from '../../resources'; 4 | 5 | // Schema for validating NASA Images API request parameters 6 | export const imagesParamsSchema = z.object({ 7 | q: z.string().min(1), 8 | media_type: z.enum(['image', 'audio', 'video']).optional(), 9 | year_start: z.string().optional(), 10 | year_end: z.string().optional(), 11 | page: z.number().int().positive().optional().default(1), 12 | page_size: z.number().int().min(1).max(100).optional().default(10) 13 | }); 14 | 15 | // Define the request parameter type based on the schema 16 | export type ImagesParams = z.infer; 17 | 18 | /** 19 | * Handle requests to NASA's Image and Video Library API 20 | */ 21 | export async function nasaImagesHandler(params: ImagesParams) { 22 | try { 23 | const { q, media_type, year_start, year_end, page, page_size } = params; 24 | 25 | // Construct request to NASA Image API 26 | const url = 'https://images-api.nasa.gov/search'; 27 | 28 | // Prepare query parameters 29 | const queryParams: Record = { 30 | q, 31 | page, 32 | page_size 33 | }; 34 | 35 | if (media_type) queryParams.media_type = media_type; 36 | if (year_start) queryParams.year_start = year_start; 37 | if (year_end) queryParams.year_end = year_end; 38 | 39 | // Make the request to NASA Images API 40 | const response = await axios.get(url, { params: queryParams, timeout: 30000 }); 41 | 42 | // Process the results and register resources 43 | return await processImageResultsWithBase64(response.data); 44 | } catch (error: any) { 45 | console.error('Error in NASA Images handler:', error); 46 | 47 | if (error.name === 'ZodError') { 48 | return { 49 | content: [{ 50 | type: "text", 51 | text: `Invalid request parameters: ${error.message}` 52 | }], 53 | isError: true 54 | }; 55 | } 56 | 57 | return { 58 | content: [{ 59 | type: "text", 60 | text: `Error fetching NASA images: ${error.message || 'Unknown error'}` 61 | }], 62 | isError: true 63 | }; 64 | } 65 | } 66 | 67 | // New async version that fetches and encodes images as base64 68 | async function processImageResultsWithBase64(data: any) { 69 | const items = data?.collection?.items || []; 70 | 71 | if (items.length === 0) { 72 | return { 73 | content: [{ 74 | type: "text", 75 | text: "No images found matching the search criteria." 76 | }], 77 | isError: false 78 | }; 79 | } 80 | 81 | const images: any[] = []; 82 | for (const item of items) { 83 | const metadata = item.data && item.data[0]; 84 | if (!metadata || !metadata.nasa_id || metadata.media_type !== 'image') continue; 85 | const nasaId = metadata.nasa_id; 86 | const title = metadata.title || 'Untitled NASA Image'; 87 | const resourceUri = `nasa://images/item?nasa_id=${nasaId}`; 88 | // Find the full-res image link (look for rel: 'orig' or the largest image) 89 | let fullResUrl = null; 90 | if (item.links && Array.isArray(item.links)) { 91 | // Try to find rel: 'orig' or the largest image 92 | const orig = item.links.find((link: any) => link.rel === 'orig'); 93 | if (orig && orig.href) fullResUrl = orig.href; 94 | else { 95 | // Fallback: use the first image link 96 | const firstImg = item.links.find((link: any) => link.render === 'image'); 97 | if (firstImg && firstImg.href) fullResUrl = firstImg.href; 98 | } 99 | } 100 | let mimeType = 'image/jpeg'; 101 | if (fullResUrl) { 102 | if (fullResUrl.endsWith('.png')) mimeType = 'image/png'; 103 | else if (fullResUrl.endsWith('.gif')) mimeType = 'image/gif'; 104 | else if (fullResUrl.endsWith('.jpg') || fullResUrl.endsWith('.jpeg')) mimeType = 'image/jpeg'; 105 | } 106 | // Fetch the image and encode as base64 107 | let base64 = null; 108 | if (fullResUrl) { 109 | try { 110 | const imageResponse = await axios.get(fullResUrl, { responseType: 'arraybuffer', timeout: 30000 }); 111 | base64 = Buffer.from(imageResponse.data).toString('base64'); 112 | } catch (err) { 113 | console.error('Failed to fetch NASA Images image for base64:', fullResUrl, err); 114 | } 115 | } 116 | addResource(resourceUri, { 117 | name: title, 118 | mimeType: "application/json", 119 | text: JSON.stringify({ 120 | item_details: metadata, 121 | full_res_url: fullResUrl, 122 | title: title, 123 | description: metadata.description || 'No description available', 124 | date_created: metadata.date_created || 'Unknown date', 125 | nasa_id: nasaId 126 | }) 127 | }); 128 | images.push({ 129 | title, 130 | base64, 131 | mimeType, 132 | url: fullResUrl 133 | }); 134 | } 135 | return { 136 | content: [ 137 | { 138 | type: "text", 139 | text: `Found ${images.length} NASA images/media items.` 140 | }, 141 | ...images.map(img => ({ 142 | type: "text", 143 | text: `![${img.title}](${img.url})` 144 | })), 145 | ...images.map(img => ({ 146 | type: "image", 147 | data: img.base64, 148 | mimeType: img.mimeType 149 | })) 150 | ], 151 | isError: false 152 | }; 153 | } 154 | 155 | // Export the handler function directly as default 156 | export default nasaImagesHandler; 157 | -------------------------------------------------------------------------------- /src/handlers/nasa/mars_rover.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { nasaApiRequest } from '../../utils/api-client'; 4 | import { MarsRoverParams } from '../setup'; 5 | import { addResource } from '../../resources'; 6 | 7 | // Schema for validating Mars Rover request parameters 8 | const marsRoverParamsSchema = z.object({ 9 | rover: z.enum(['curiosity', 'opportunity', 'perseverance', 'spirit']), 10 | sol: z.number().int().nonnegative().optional(), 11 | earth_date: z.string().optional(), 12 | camera: z.string().optional(), 13 | page: z.number().int().positive().optional() 14 | }); 15 | 16 | /** 17 | * Handle requests for NASA's Mars Rover Photos API 18 | */ 19 | export async function nasaMarsRoverHandler(params: MarsRoverParams) { 20 | try { 21 | const { rover, ...queryParams } = params; 22 | 23 | // Call the NASA Mars Rover Photos API 24 | const result = await nasaApiRequest(`/mars-photos/api/v1/rovers/${rover}/photos`, queryParams); 25 | 26 | // Process the results and register resources 27 | return processRoverResults(result, rover); 28 | } catch (error: any) { 29 | console.error('Error in Mars Rover handler:', error); 30 | 31 | if (error.name === 'ZodError') { 32 | return { 33 | content: [{ 34 | type: "text", 35 | text: `Invalid request parameters: ${JSON.stringify(error.errors)}` 36 | }], 37 | isError: true 38 | }; 39 | } 40 | 41 | return { 42 | content: [{ 43 | type: "text", 44 | text: `Error fetching Mars Rover photos: ${error.message || 'Unknown error'}` 45 | }], 46 | isError: true 47 | }; 48 | } 49 | } 50 | 51 | /** 52 | * Process the Mars Rover API results, register resources, and format the response 53 | */ 54 | async function processRoverResults(data: any, rover: string) { 55 | const photos = data.photos || []; 56 | const resources = []; 57 | // Collect base64 image data for direct display 58 | const images: Array<{ title: string; url: string; data: string; mimeType: string }> = []; 59 | 60 | if (photos.length === 0) { 61 | return { 62 | content: [{ 63 | type: "text", 64 | text: `No photos found for rover ${rover} with the specified parameters.` 65 | }], 66 | isError: false 67 | }; 68 | } 69 | 70 | // Register each photo as a resource 71 | for (const photo of photos) { 72 | const photoId = photo.id.toString(); 73 | const resourceUri = `nasa://mars_rover/photo?rover=${rover}&id=${photoId}`; 74 | 75 | try { 76 | // Fetch the actual image data 77 | const imageResponse = await axios({ 78 | url: photo.img_src, 79 | responseType: 'arraybuffer', 80 | timeout: 30000 81 | }); 82 | 83 | // Convert image data to Base64 84 | const imageBase64 = Buffer.from(imageResponse.data).toString('base64'); 85 | 86 | // Register the resource with binary data in the blob field 87 | addResource(resourceUri, { 88 | name: `Mars Rover Photo ${photoId}`, 89 | mimeType: "image/jpeg", 90 | // Store metadata as text for reference 91 | text: JSON.stringify({ 92 | photo_id: photoId, 93 | rover: rover, 94 | camera: photo.camera?.name || 'Unknown', 95 | earth_date: photo.earth_date, 96 | sol: photo.sol, 97 | img_src: photo.img_src 98 | }), 99 | // Store the actual image data as a blob 100 | blob: Buffer.from(imageResponse.data) 101 | }); 102 | // Keep base64 data for direct response 103 | images.push({ title: `Mars Rover Photo ${photoId}`, url: photo.img_src, data: imageBase64, mimeType: "image/jpeg" }); 104 | } catch (error) { 105 | console.error(`Error fetching image for rover photo ${photoId}:`, error); 106 | 107 | // If fetching fails, register with just the metadata and URL 108 | addResource(resourceUri, { 109 | name: `Mars Rover Photo ${photoId}`, 110 | mimeType: "image/jpeg", 111 | text: JSON.stringify({ 112 | photo_id: photoId, 113 | rover: rover, 114 | camera: photo.camera?.name || 'Unknown', 115 | img_src: photo.img_src, 116 | earth_date: photo.earth_date, 117 | sol: photo.sol, 118 | fetch_error: (error as Error).message 119 | }) 120 | }); 121 | } 122 | 123 | resources.push({ 124 | title: `Mars Rover Photo ${photoId}`, 125 | description: `Photo taken by ${rover} rover on Mars`, 126 | resource_uri: resourceUri 127 | }); 128 | } 129 | 130 | // Format the response for MCP 131 | return { 132 | content: [ 133 | { 134 | type: "text", 135 | text: `Found ${photos.length} photos from Mars rover ${rover}.` 136 | }, 137 | { 138 | type: "text", 139 | text: JSON.stringify(resources, null, 2) 140 | }, 141 | // Include direct image links and binary data 142 | ...images.map(img => ({ type: "text", text: `![${img.title}](${img.url})` })), 143 | ...images.map(img => ({ type: "image", data: img.data, mimeType: img.mimeType })), 144 | ], 145 | isError: false 146 | }; 147 | } 148 | 149 | // Export with all possible names that handleToolCall might be looking for 150 | // Primary export should match file name convention 151 | export const mars_roverHandler = nasaMarsRoverHandler; 152 | export const marsRoverHandler = nasaMarsRoverHandler; 153 | 154 | // Keep these secondary exports for compatibility 155 | export const nasaMars_RoverHandler = nasaMarsRoverHandler; 156 | 157 | // Default export 158 | export default nasaMarsRoverHandler; 159 | -------------------------------------------------------------------------------- /docs/inspector-test-examples.md: -------------------------------------------------------------------------------- 1 | # NASA MCP Server - Inspector Test Examples 2 | 3 | This document provides example requests you can copy and paste into the MCP Inspector to test each of the NASA APIs implemented in our server. 4 | 5 | ## Running the Inspector 6 | 7 | To run the MCP Inspector with our NASA MCP server: 8 | 9 | ```bash 10 | # Run the provided script 11 | ./scripts/test-with-inspector.sh 12 | 13 | # Or run manually 14 | npx @modelcontextprotocol/inspector node dist/index.js 15 | ``` 16 | 17 | ## Table of Contents 18 | - [Server Information](#server-information) 19 | - [NASA APIs](#nasa-apis) 20 | - [APOD (Astronomy Picture of the Day)](#apod) 21 | - [EPIC (Earth Polychromatic Imaging Camera)](#epic) 22 | - [NEO (Near Earth Object Web Service)](#neo) 23 | - [GIBS (Global Imagery Browse Services)](#gibs) 24 | - [CMR (Common Metadata Repository)](#cmr) 25 | - [FIRMS (Fire Information)](#firms) 26 | - [NASA Image and Video Library](#nasa-images) 27 | - [Exoplanet Archive](#exoplanet) 28 | - [DONKI (Space Weather Database)](#donki) 29 | - [Mars Rover Photos](#mars-rover) 30 | - [EONET (Earth Observatory Events)](#eonet) 31 | - [NASA Sounds API](#sounds) 32 | - [POWER (Energy Resources)](#power) 33 | - [JPL APIs](#jpl-apis) 34 | - [SBDB (Small-Body Database)](#sbdb) 35 | - [Fireball Data](#fireball) 36 | - [Scout API](#scout) 37 | 38 | ## Server Information 39 | 40 | Get the manifest of available APIs: 41 | 42 | ```json 43 | { 44 | "method": "tools/manifest", 45 | "params": {} 46 | } 47 | ``` 48 | 49 | ## NASA APIs 50 | 51 | ### APOD 52 | 53 | Get the Astronomy Picture of the Day: 54 | 55 | ```json 56 | { 57 | "method": "nasa/apod", 58 | "params": { 59 | "date": "2023-01-01" 60 | } 61 | } 62 | ``` 63 | 64 | Get a random APOD: 65 | 66 | ```json 67 | { 68 | "method": "nasa/apod", 69 | "params": { 70 | "count": 1 71 | } 72 | } 73 | ``` 74 | 75 | ### EPIC 76 | 77 | Get the latest EPIC images: 78 | 79 | ```json 80 | { 81 | "method": "nasa/epic", 82 | "params": { 83 | "collection": "natural" 84 | } 85 | } 86 | ``` 87 | 88 | ### NEO 89 | 90 | Get Near Earth Objects for a date range: 91 | 92 | ```json 93 | { 94 | "method": "nasa/neo", 95 | "params": { 96 | "start_date": "2023-01-01", 97 | "end_date": "2023-01-02" 98 | } 99 | } 100 | ``` 101 | 102 | ### GIBS 103 | 104 | Get a satellite imagery layer: 105 | 106 | ```json 107 | { 108 | "method": "nasa/gibs", 109 | "params": { 110 | "layer": "MODIS_Terra_CorrectedReflectance_TrueColor", 111 | "date": "2023-01-01" 112 | } 113 | } 114 | ``` 115 | 116 | ### CMR 117 | 118 | Basic collection search: 119 | 120 | ```json 121 | { 122 | "method": "nasa/cmr", 123 | "params": { 124 | "keyword": "hurricane", 125 | "limit": 2 126 | } 127 | } 128 | ``` 129 | 130 | Advanced collection search with spatial parameters: 131 | 132 | ```json 133 | { 134 | "method": "nasa/cmr", 135 | "params": { 136 | "search_type": "collections", 137 | "platform": "Terra", 138 | "bbox": "-180,-90,180,90", 139 | "limit": 5, 140 | "include_facets": true 141 | } 142 | } 143 | ``` 144 | 145 | Granule search: 146 | 147 | ```json 148 | { 149 | "method": "nasa/cmr", 150 | "params": { 151 | "search_type": "granules", 152 | "concept_id": "C1000000000-ORNL_DAAC", 153 | "limit": 3 154 | } 155 | } 156 | ``` 157 | 158 | ### FIRMS 159 | 160 | Get fire data: 161 | 162 | ```json 163 | { 164 | "method": "nasa/firms", 165 | "params": { 166 | "area": "world", 167 | "days": 1 168 | } 169 | } 170 | ``` 171 | 172 | ### NASA Images 173 | 174 | Search NASA's image library: 175 | 176 | ```json 177 | { 178 | "method": "nasa/images", 179 | "params": { 180 | "q": "apollo 11", 181 | "media_type": "image", 182 | "year_start": 1969, 183 | "year_end": 1970 184 | } 185 | } 186 | ``` 187 | 188 | ### Exoplanet 189 | 190 | Search for exoplanets: 191 | 192 | ```json 193 | { 194 | "method": "nasa/exoplanet", 195 | "params": { 196 | "select": "pl_name,pl_masse,st_dist", 197 | "where": "pl_masse>1", 198 | "order": "pl_masse", 199 | "limit": 5 200 | } 201 | } 202 | ``` 203 | 204 | ### DONKI 205 | 206 | Get Coronal Mass Ejection data: 207 | 208 | ```json 209 | { 210 | "method": "nasa/donki", 211 | "params": { 212 | "type": "cme", 213 | "startDate": "2022-01-01", 214 | "endDate": "2022-01-10" 215 | } 216 | } 217 | ``` 218 | 219 | ### Mars Rover 220 | 221 | Get photos from Mars Perseverance: 222 | 223 | ```json 224 | { 225 | "method": "nasa/mars-rover", 226 | "params": { 227 | "rover": "perseverance", 228 | "sol": 100 229 | } 230 | } 231 | ``` 232 | 233 | ### EONET 234 | 235 | Get natural event data: 236 | 237 | ```json 238 | { 239 | "method": "nasa/eonet", 240 | "params": { 241 | "category": "wildfires", 242 | "days": 20, 243 | "status": "open" 244 | } 245 | } 246 | ``` 247 | 248 | ### Sounds 249 | 250 | Get space sounds: 251 | 252 | ```json 253 | { 254 | "method": "nasa/sounds", 255 | "params": { 256 | "q": "voyager", 257 | "limit": 3 258 | } 259 | } 260 | ``` 261 | 262 | ### POWER 263 | 264 | Get solar and meteorological data: 265 | 266 | ```json 267 | { 268 | "method": "nasa/power", 269 | "params": { 270 | "parameters": "T2M,PRECTOTCORR,WS10M", 271 | "community": "re", 272 | "latitude": 40.7128, 273 | "longitude": -74.0060, 274 | "start": "20220101", 275 | "end": "20220107" 276 | } 277 | } 278 | ``` 279 | 280 | ## JPL APIs 281 | 282 | ### SBDB 283 | 284 | Query the Small-Body Database: 285 | 286 | ```json 287 | { 288 | "method": "jpl/sbdb", 289 | "params": { 290 | "sstr": "433", 291 | "full_precision": true 292 | } 293 | } 294 | ``` 295 | 296 | ### Fireball 297 | 298 | Get fireball data: 299 | 300 | ```json 301 | { 302 | "method": "jpl/fireball", 303 | "params": { 304 | "date_min": "2022-01-01", 305 | "limit": 5 306 | } 307 | } 308 | ``` 309 | 310 | ### Scout 311 | 312 | Get Scout data: 313 | 314 | ```json 315 | { 316 | "method": "jpl/scout", 317 | "params": {} 318 | } 319 | ``` -------------------------------------------------------------------------------- /src/handlers/setup.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { z } from 'zod'; 3 | import axios from 'axios'; 4 | 5 | // NASA API handlers 6 | import { nasaApodHandler, apodParamsSchema, ApodParams } from './nasa/apod'; 7 | import { nasaEpicHandler, epicParamsSchema, EpicParams } from './nasa/epic'; 8 | import { neoParamsSchema, NeoParams } from './nasa/neo'; 9 | import { nasaGibsHandler, gibsParamsSchema, GibsParams } from './nasa/gibs'; 10 | import { nasaCmrHandler, cmrParamsSchema, CmrParams } from './nasa/cmr'; 11 | import { nasaFirmsHandler, firmsParamsSchema, FirmsParams } from './nasa/firms'; 12 | import { nasaImagesHandler, imagesParamsSchema, ImagesParams } from './nasa/images'; 13 | import { nasaExoplanetHandler, exoplanetParamsSchema, ExoplanetParams } from './nasa/exoplanet'; 14 | import { nasaDonkiHandler } from './nasa/donki'; 15 | import { nasaMarsRoverHandler } from './nasa/mars_rover'; 16 | import { nasaEonetHandler } from './nasa/eonet'; 17 | import { nasaPowerHandler, powerParamsSchema, PowerParams } from './nasa/power'; 18 | import { nasaEarthHandler, earthParamsSchema, EarthParams } from './nasa/earth'; 19 | 20 | // JPL API handlers 21 | import { jplSbdbHandler, sbdbParamsSchema, SbdbParams } from './jpl/sbdb'; 22 | import { jplFireballHandler, fireballParamsSchema, FireballParams } from './jpl/fireball'; 23 | import { jplScoutHandler } from './jpl/scout'; 24 | 25 | // Define schemas for all NASA API endpoints 26 | const ApodSchema = z.object({ 27 | date: z.string().optional(), 28 | start_date: z.string().optional(), 29 | end_date: z.string().optional(), 30 | count: z.number().optional(), 31 | thumbs: z.boolean().optional() 32 | }); 33 | 34 | const EpicSchema = z.object({ 35 | collection: z.enum(['natural', 'enhanced']).optional(), 36 | date: z.string().optional() 37 | }); 38 | 39 | const NeoSchema = z.object({ 40 | start_date: z.string(), 41 | end_date: z.string() 42 | }); 43 | 44 | const GibsSchema = z.object({ 45 | layer: z.string(), 46 | date: z.string(), 47 | format: z.enum(['png', 'jpg', 'jpeg']).optional(), 48 | resolution: z.number().optional() 49 | }); 50 | 51 | const CmrSchema = z.object({ 52 | keyword: z.string(), 53 | limit: z.number().optional(), 54 | page: z.number().optional(), 55 | sort_key: z.string().optional() 56 | }); 57 | 58 | const FirmsSchema = z.object({ 59 | latitude: z.number(), 60 | longitude: z.number(), 61 | days: z.number().optional() 62 | }); 63 | 64 | const ImagesSchema = z.object({ 65 | q: z.string(), 66 | media_type: z.enum(['image', 'video', 'audio']).optional(), 67 | year_start: z.string().optional(), 68 | year_end: z.string().optional(), 69 | page: z.number().optional() 70 | }); 71 | 72 | const ExoplanetSchema = z.object({ 73 | table: z.string(), 74 | select: z.string().optional(), 75 | where: z.string().optional(), 76 | order: z.string().optional(), 77 | limit: z.number().optional() 78 | }); 79 | 80 | const EarthSchema = z.object({ 81 | lon: z.number().or(z.string().regex(/^-?\d+(\.\d+)?$/).transform(Number)), 82 | lat: z.number().or(z.string().regex(/^-?\d+(\.\d+)?$/).transform(Number)), 83 | date: z.string().optional(), 84 | dim: z.number().optional(), 85 | cloud_score: z.boolean().optional() 86 | }); 87 | 88 | const SbdbSchema = z.object({ 89 | search: z.string() 90 | }); 91 | 92 | const FireballSchema = z.object({ 93 | date_min: z.string().optional(), 94 | date_max: z.string().optional(), 95 | energy_min: z.number().optional() 96 | }); 97 | 98 | const ScoutSchema = z.object({ 99 | tdes: z.string().describe("Object temporary designation (e.g., P21Eolo)").optional(), 100 | orbit_id: z.string().describe("Scout internal orbit ID").optional(), 101 | limit: z.number().int().positive().describe("Limit number of results").optional(), 102 | file: z.enum(['summary', 'ephem', 'obs', 'crit', 'all']).describe("Type of data file to return").optional(), 103 | plot: z.boolean().describe("Include plots in the response").optional(), 104 | summary: z.boolean().describe("Include summary data in the response").optional() 105 | }); 106 | 107 | // Define schemas for added APIs 108 | const DonkiSchema = z.object({ 109 | type: z.enum(['cme', 'cmea', 'gst', 'ips', 'flr', 'sep', 'mpc', 'rbe', 'hss', 'wsa', 'notifications']), 110 | startDate: z.string().optional(), 111 | endDate: z.string().optional() 112 | }); 113 | 114 | const MarsRoverSchema = z.object({ 115 | rover: z.enum(['curiosity', 'opportunity', 'perseverance', 'spirit']), 116 | sol: z.number().int().nonnegative().optional(), 117 | earth_date: z.string().optional(), 118 | camera: z.string().optional(), 119 | page: z.number().int().positive().optional() 120 | }); 121 | 122 | const EonetSchema = z.object({ 123 | category: z.string().optional(), 124 | days: z.number().int().positive().optional(), 125 | source: z.string().optional(), 126 | status: z.enum(['open', 'closed', 'all']).optional(), 127 | limit: z.number().int().positive().optional() 128 | }); 129 | 130 | // Convert the Express handlers to MCP handlers 131 | export const donkiParamsSchema = DonkiSchema; 132 | export type DonkiParams = z.infer; 133 | 134 | export const marsRoverParamsSchema = MarsRoverSchema; 135 | export type MarsRoverParams = z.infer; 136 | 137 | export const eonetParamsSchema = EonetSchema; 138 | export type EonetParams = z.infer; 139 | 140 | export const scoutParamsSchema = ScoutSchema; 141 | export type ScoutParams = z.infer; 142 | 143 | /** 144 | * Setup MCP handlers for NASA APIs 145 | * Note: With our new architecture, the actual CallToolRequestSchema handler is now 146 | * in the main index.ts file. This function simply registers the handlers for 147 | * validating parameters, but doesn't need to handle the actual tool execution. 148 | */ 149 | export function setupHandlers(context: Server) { 150 | // Our new architecture already handles the tool calls 151 | // This function is now mostly a placeholder, but could be used 152 | // for additional server setup if needed 153 | 154 | // Register notifications handler if needed 155 | context.setRequestHandler( 156 | z.object({ method: z.literal("nasa/subscribe") }), 157 | async (request) => { 158 | return { success: true }; 159 | } 160 | ); 161 | } -------------------------------------------------------------------------------- /src/tests/custom-client/nasa-mcp-test.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | import { z } from "zod"; 4 | 5 | async function testNasaApis() { 6 | console.log('Starting NASA MCP API tests...'); 7 | 8 | // Track successful tests 9 | const successfulTests: string[] = []; 10 | 11 | // Create a transport to communicate with the server 12 | const transport = new StdioClientTransport({ 13 | command: "node", 14 | args: ["dist/index.js"] 15 | }); 16 | 17 | // Create an MCP client 18 | const client = new Client( 19 | { 20 | name: "nasa-mcp-test-client", 21 | version: "1.0.0" 22 | }, 23 | { 24 | capabilities: { 25 | tools: {} 26 | } 27 | } 28 | ); 29 | 30 | // Define schemas for validation 31 | const toolsManifestSchema = z.object({ 32 | apis: z.array(z.object({ 33 | name: z.string(), 34 | id: z.string(), 35 | description: z.string().optional() 36 | })) 37 | }); 38 | 39 | const anySchema = z.any(); 40 | 41 | try { 42 | // Connect to the server 43 | await client.connect(transport); 44 | console.log('Client connected successfully'); 45 | 46 | // Get tools manifest 47 | const toolsRequest = await client.request( 48 | { method: "tools/manifest", params: {} }, 49 | toolsManifestSchema 50 | ); 51 | console.log('Tools manifest received:', JSON.stringify(toolsRequest, null, 2)); 52 | 53 | // Test APOD API 54 | try { 55 | console.log('\nTesting NASA APOD API...'); 56 | const apodResult = await client.request( 57 | { method: "nasa/apod", params: { date: "2023-01-01" } }, 58 | anySchema 59 | ); 60 | successfulTests.push('APOD'); 61 | console.log(`APOD Result: ${(apodResult as any).title}`); 62 | } catch (error) { 63 | console.error('APOD test failed:', error); 64 | } 65 | 66 | // Test EPIC API 67 | try { 68 | console.log('\nTesting NASA EPIC API...'); 69 | // Use a date we know has data from the documentation examples 70 | const epicResult = await client.request( 71 | { method: "nasa/epic", params: { collection: "natural", date: "2015-10-31" } }, 72 | anySchema 73 | ); 74 | successfulTests.push('EPIC'); 75 | console.log(`EPIC Result: Retrieved ${(epicResult as any).length} images`); 76 | } catch (error) { 77 | console.error('EPIC test failed:', error); 78 | } 79 | 80 | // Test NEO API 81 | try { 82 | console.log('\nTesting NASA NEO API...'); 83 | const neoResult = await client.request( 84 | { 85 | method: "nasa/neo", 86 | params: { 87 | start_date: "2023-01-01", 88 | end_date: "2023-01-07" 89 | } 90 | }, 91 | anySchema 92 | ); 93 | successfulTests.push('NEO'); 94 | console.log(`NEO Result: Found ${(neoResult as any).element_count} near-Earth objects`); 95 | } catch (error) { 96 | console.error('NEO test failed:', error); 97 | } 98 | 99 | // Test Mars Rover Photos API 100 | try { 101 | console.log('\nTesting NASA Mars Rover Photos API...'); 102 | const marsRoverResult = await client.request( 103 | { 104 | method: "nasa/mars-rover", 105 | params: { 106 | rover: "curiosity", 107 | sol: 1000 108 | } 109 | }, 110 | anySchema 111 | ); 112 | successfulTests.push('Mars Rover'); 113 | console.log(`Mars Rover Result: Retrieved ${(marsRoverResult as any).photos?.length || 0} photos`); 114 | } catch (error) { 115 | console.error('Mars Rover test failed:', error); 116 | } 117 | 118 | // Test Exoplanet API 119 | try { 120 | console.log('\nTesting NASA Exoplanet API...'); 121 | const exoplanetResult = await client.request( 122 | { 123 | method: "nasa/exoplanet", 124 | params: { 125 | table: "ps", 126 | // Simplified query with just table and limit 127 | limit: 10 128 | } 129 | }, 130 | anySchema 131 | ); 132 | successfulTests.push('Exoplanet'); 133 | console.log(`Exoplanet Result: Retrieved ${(exoplanetResult as any).results?.length || 0} exoplanets`); 134 | } catch (error) { 135 | console.error('Exoplanet test failed:', error); 136 | } 137 | 138 | // Test FIRMS API 139 | try { 140 | console.log('\nTesting NASA FIRMS API...'); 141 | const firmsResult = await client.request( 142 | { 143 | method: "nasa/firms", 144 | params: { 145 | latitude: 37.7749, // San Francisco 146 | longitude: -122.4194, 147 | radius: 5.0, 148 | days: 1 149 | } 150 | }, 151 | anySchema 152 | ); 153 | successfulTests.push('FIRMS'); 154 | console.log(`FIRMS Result: Retrieved ${(firmsResult as any).results?.length || 0} fire data points`); 155 | } catch (error) { 156 | console.error('FIRMS test failed:', error); 157 | } 158 | 159 | // Test Images Library API 160 | try { 161 | console.log('\nTesting NASA Images Library API...'); 162 | const imagesResult = await client.request( 163 | { method: "nasa/images", params: { q: "moon landing" } }, 164 | anySchema 165 | ); 166 | successfulTests.push('Images'); 167 | console.log(`Images Result: Retrieved ${(imagesResult as any).collection?.items?.length || 0} images`); 168 | } catch (error) { 169 | console.error('Images test failed:', error); 170 | } 171 | 172 | // Test EONET API 173 | try { 174 | console.log('\nTesting NASA EONET API...'); 175 | const eonetResult = await client.request( 176 | { method: "nasa/eonet", params: { status: "all", limit: 10, days: 60 } }, 177 | anySchema 178 | ); 179 | successfulTests.push('EONET'); 180 | console.log(`EONET Result: Retrieved ${(eonetResult as any).events?.length || 0} events`); 181 | } catch (error) { 182 | console.error('EONET test failed:', error); 183 | } 184 | 185 | // Summary of test results 186 | console.log('\n======= TEST SUMMARY ======='); 187 | console.log(`Total APIs tested: ${successfulTests.length}`); 188 | console.log(`Successful tests: ${successfulTests.join(', ')}`); 189 | console.log('============================\n'); 190 | 191 | console.log('All tests completed!'); 192 | process.exit(0); 193 | } catch (error) { 194 | console.error('Test failed:', error); 195 | process.exit(1); 196 | } 197 | } 198 | 199 | // Run the test 200 | testNasaApis().catch(error => { 201 | console.error('Test failed:', error); 202 | process.exit(1); 203 | }); -------------------------------------------------------------------------------- /src/utils/api-client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | import dotenv from 'dotenv'; 3 | import path from 'path'; 4 | import { transformParamsToHyphenated } from './param-transformer'; 5 | 6 | // Try to load environment variables from .env file with absolute path 7 | dotenv.config(); 8 | // Also try with explicit path as fallback 9 | dotenv.config({ path: path.resolve(process.cwd(), '.env') }); 10 | 11 | // NASA API Base URLs 12 | export const NASA_API_BASE_URL = 'https://api.nasa.gov'; 13 | export const JPL_SSD_API_BASE_URL = 'https://ssd-api.jpl.nasa.gov'; 14 | 15 | /** 16 | * Make a request to a NASA API endpoint 17 | */ 18 | export async function nasaApiRequest( 19 | endpoint: string, 20 | params: Record = {}, 21 | options: AxiosRequestConfig = {} 22 | ) { 23 | try { 24 | // First check for API key in environment variables 25 | let apiKey = process.env.NASA_API_KEY; 26 | 27 | // If not found, try loading from .env file with explicit path 28 | if (!apiKey) { 29 | try { 30 | const envPath = path.resolve(process.cwd(), '.env'); 31 | dotenv.config({ path: envPath }); 32 | apiKey = process.env.NASA_API_KEY; 33 | } catch (error) { 34 | console.error('Error loading .env file:', error); 35 | } 36 | } 37 | 38 | if (!apiKey) { 39 | return { 40 | isError: true, 41 | content: [{ 42 | type: "text", 43 | text: 'NASA API key not found. Please set NASA_API_KEY in .env file' 44 | }] 45 | }; 46 | } 47 | 48 | const response = await axios({ 49 | url: `${NASA_API_BASE_URL}${endpoint}`, 50 | params: { 51 | ...params, 52 | api_key: apiKey 53 | }, 54 | timeout: 30000, // 30 second timeout 55 | ...options 56 | }); 57 | 58 | return response.data; 59 | } catch (error: any) { 60 | console.error(`Error calling NASA API (${endpoint}):`, error.message); 61 | 62 | if (error.response) { 63 | // The request was made and the server responded with a status code 64 | // that falls out of the range of 2xx 65 | console.error('Response status:', error.response.status); 66 | console.error('Response headers:', JSON.stringify(error.response.headers)); 67 | console.error('Response data:', JSON.stringify(error.response.data).substring(0, 200)); 68 | 69 | return { 70 | isError: true, 71 | content: [{ 72 | type: "text", 73 | text: `NASA API error (${error.response.status}): ${error.response.data.error?.message || 'Unknown error'}` 74 | }] 75 | }; 76 | } else if (error.request) { 77 | // The request was made but no response was received 78 | console.error('Request details:'); 79 | console.error('- URL:', error.request._currentUrl || 'Not available'); 80 | console.error('- Method:', error.request.method || 'Not available'); 81 | console.error('- Headers:', error.request._header || 'Not available'); 82 | console.error('- Timeout:', error.request.timeout || 'Not available'); 83 | 84 | return { 85 | isError: true, 86 | content: [{ 87 | type: "text", 88 | text: `NASA API network error: No response received or request timed out. URL: ${error.request._currentUrl || 'Unknown'}` 89 | }] 90 | }; 91 | } else { 92 | // Something happened in setting up the request that triggered an Error 93 | return { 94 | isError: true, 95 | content: [{ 96 | type: "text", 97 | text: `NASA API request error: ${error.message}` 98 | }] 99 | }; 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Make a request to a JPL SSD API endpoint 106 | */ 107 | export async function jplApiRequest( 108 | endpoint: string, 109 | params: Record = {}, 110 | options: AxiosRequestConfig = {} 111 | ) { 112 | try { 113 | // JPL endpoints might use the NASA API key, but check exceptions like Scout 114 | let apiKey = process.env.NASA_API_KEY; 115 | 116 | // Transform parameter names from underscore to hyphenated format 117 | // as the JPL APIs expect hyphenated parameter names 118 | let paramsToSend = transformParamsToHyphenated(params); 119 | 120 | // Only add api_key if required and available, and not for scout.api 121 | if (endpoint !== '/scout.api' && apiKey) { 122 | paramsToSend.api_key = apiKey; 123 | } else if (endpoint !== '/scout.api' && !apiKey) { 124 | // If other JPL endpoints require a key but it's missing, try loading .env 125 | try { 126 | const envPath = path.resolve(process.cwd(), '.env'); 127 | dotenv.config({ path: envPath }); 128 | apiKey = process.env.NASA_API_KEY; 129 | if (apiKey) { 130 | paramsToSend.api_key = apiKey; 131 | } else { 132 | // Return error if key is needed but not found AFTER trying .env 133 | return { 134 | isError: true, 135 | content: [{ 136 | type: "text", 137 | text: 'NASA API key not found for JPL endpoint. Please set NASA_API_KEY in .env file' 138 | }] 139 | }; 140 | } 141 | } catch (error) { 142 | console.error('Error loading .env file for JPL key:', error); 143 | // Proceed without key for now, endpoint might not strictly require it 144 | } 145 | } // else: if endpoint is /scout.api, we intentionally DO NOT add the api_key 146 | 147 | const response = await axios({ 148 | url: `${JPL_SSD_API_BASE_URL}${endpoint}`, 149 | params: paramsToSend, // Use the potentially modified params object 150 | timeout: 30000, // 30 second timeout 151 | ...options 152 | }); 153 | 154 | return response.data; 155 | } catch (error: any) { 156 | console.error(`Error calling JPL API (${endpoint}):`, error.message); 157 | 158 | if (error.response) { 159 | console.error('Response status:', error.response.status); 160 | console.error('Response data:', JSON.stringify(error.response.data).substring(0, 200)); 161 | 162 | return { 163 | isError: true, 164 | content: [{ 165 | type: "text", 166 | text: `JPL API error (${error.response.status}): ${error.response.data.message || 'Unknown error'}` 167 | }] 168 | }; 169 | } else if (error.request) { 170 | console.error('Request details:'); 171 | console.error('- URL:', error.request._currentUrl || 'Not available'); 172 | console.error('- Method:', error.request.method || 'Not available'); 173 | console.error('- Headers:', error.request._header || 'Not available'); 174 | console.error('- Timeout:', error.request.timeout || 'Not available'); 175 | 176 | return { 177 | isError: true, 178 | content: [{ 179 | type: "text", 180 | text: `JPL API network error: No response received. URL: ${error.request._currentUrl || 'Unknown'}` 181 | }] 182 | }; 183 | } else { 184 | return { 185 | isError: true, 186 | content: [{ 187 | type: "text", 188 | text: `JPL API request error: ${error.message}` 189 | }] 190 | }; 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /src/handlers/nasa/epic.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { nasaApiRequest } from '../../utils/api-client'; 4 | import { addResource } from '../../resources'; 5 | 6 | // Define the EPIC API base URL 7 | const EPIC_API_BASE_URL = 'https://epic.gsfc.nasa.gov/api'; 8 | const EPIC_IMAGE_BASE_URL = 'https://epic.gsfc.nasa.gov/archive'; 9 | 10 | // Schema for validating EPIC request parameters 11 | export const epicParamsSchema = z.object({ 12 | collection: z.enum(['natural', 'enhanced']).optional().default('natural'), 13 | date: z.string().optional(), 14 | }); 15 | 16 | // Define the request parameter type based on the schema 17 | export type EpicParams = z.infer; 18 | 19 | /** 20 | * Process EPIC API results and format them for MCP 21 | * @param epicData The raw EPIC data from the API 22 | * @param collection The collection type (natural or enhanced) 23 | * @returns Formatted results with summary and image data 24 | */ 25 | async function processEpicResults(epicData: any[], collection: string) { 26 | if (!Array.isArray(epicData) || epicData.length === 0) { 27 | return { 28 | summary: "No EPIC data available for the specified parameters.", 29 | images: [] 30 | }; 31 | } 32 | 33 | // Extract date information from the first image 34 | const firstImage = epicData[0]; 35 | const date = firstImage.date || 'unknown date'; 36 | 37 | // Get image date parts for URL construction 38 | const dateStr = firstImage.date.split(' ')[0]; 39 | const [year, month, day] = dateStr.split('-'); 40 | 41 | // Collect image data including base64 for direct display 42 | const images: Array<{ identifier: string; caption: string; imageUrl: string; resourceUri: string; base64?: string; mimeType?: string; error?: string }> = []; 43 | 44 | for (const img of epicData) { 45 | // Construct the image URL according to NASA's format 46 | const imageUrl = `${EPIC_IMAGE_BASE_URL}/${collection}/${year}/${month}/${day}/png/${img.image}.png`; 47 | 48 | // Create a unique resource URI for this image 49 | const resourceUri = `nasa://epic/image/${collection}/${img.identifier}`; 50 | 51 | try { 52 | // Fetch the actual image data 53 | const imageResponse = await axios({ 54 | url: imageUrl, 55 | responseType: 'arraybuffer', 56 | timeout: 30000 57 | }); 58 | 59 | // Convert image data to Base64 for direct response 60 | const imageBase64 = Buffer.from(imageResponse.data).toString('base64'); 61 | 62 | // Register this image as a resource with binary data 63 | addResource(resourceUri, { 64 | name: `NASA EPIC Earth Image - ${img.identifier}`, 65 | mimeType: "image/png", 66 | // Store metadata as text 67 | text: JSON.stringify({ 68 | id: img.identifier, 69 | date: img.date, 70 | caption: img.caption || "Earth view from DSCOVR satellite", 71 | imageUrl: imageUrl, 72 | centroid_coordinates: img.centroid_coordinates, 73 | dscovr_j2000_position: img.dscovr_j2000_position, 74 | lunar_j2000_position: img.lunar_j2000_position, 75 | sun_j2000_position: img.sun_j2000_position, 76 | attitude_quaternions: img.attitude_quaternions 77 | }), 78 | // Store actual image data as blob 79 | blob: Buffer.from(imageResponse.data) 80 | }); 81 | 82 | // Keep data for direct response 83 | images.push({ 84 | identifier: img.identifier, 85 | caption: img.caption || "Earth view from DSCOVR satellite", 86 | imageUrl: imageUrl, 87 | resourceUri: resourceUri, 88 | base64: imageBase64, 89 | mimeType: "image/png" 90 | }); 91 | } catch (error) { 92 | console.error(`Error fetching EPIC image ${img.identifier}:`, error); 93 | 94 | // If fetch fails, register with just the metadata 95 | addResource(resourceUri, { 96 | name: `NASA EPIC Earth Image - ${img.identifier}`, 97 | mimeType: "image/png", 98 | text: JSON.stringify({ 99 | id: img.identifier, 100 | date: img.date, 101 | caption: img.caption || "Earth view from DSCOVR satellite", 102 | imageUrl: imageUrl, 103 | centroid_coordinates: img.centroid_coordinates, 104 | dscovr_j2000_position: img.dscovr_j2000_position, 105 | lunar_j2000_position: img.lunar_j2000_position, 106 | sun_j2000_position: img.sun_j2000_position, 107 | attitude_quaternions: img.attitude_quaternions, 108 | fetch_error: (error as Error).message 109 | }) 110 | }); 111 | 112 | images.push({ 113 | identifier: img.identifier, 114 | caption: img.caption || "Earth view from DSCOVR satellite", 115 | imageUrl: imageUrl, 116 | resourceUri: resourceUri, 117 | error: "Failed to fetch image data" 118 | }); 119 | } 120 | } 121 | 122 | return { 123 | summary: `EPIC Earth imagery from ${date} - Collection: ${collection} - ${images.length} images available`, 124 | images: images 125 | }; 126 | } 127 | 128 | /** 129 | * Handle requests for NASA's Earth Polychromatic Imaging Camera (EPIC) API 130 | */ 131 | export async function nasaEpicHandler(params: EpicParams) { 132 | try { 133 | // Parse the request parameters 134 | const { collection, date } = params; 135 | 136 | // Determine the endpoint based on parameters 137 | let endpoint = `/${collection}`; 138 | if (date) { 139 | endpoint += `/date/${date}`; 140 | } 141 | 142 | // Try to fetch EPIC data with timeout of 30 seconds 143 | const response = await axios.get(`${EPIC_API_BASE_URL}${endpoint}`, { 144 | timeout: 30000 145 | }); 146 | 147 | const epicData = response.data; 148 | 149 | // Process the results 150 | if (epicData && epicData.length > 0) { 151 | const results = await processEpicResults(epicData, collection); 152 | 153 | return { 154 | content: [ 155 | { type: "text", text: results.summary }, 156 | // Existing resource URI entries 157 | ...results.images.map(img => ({ type: "text", text: `![${img.caption}](${img.resourceUri})` })), 158 | // Direct image URL markdown entries 159 | ...results.images.map(img => ({ type: "text", text: `![${img.caption}](${img.imageUrl})` })), 160 | // Embedded binary images 161 | ...results.images 162 | .filter(img => img.base64) 163 | .map(img => ({ type: "image", data: img.base64!, mimeType: img.mimeType! })), 164 | ], 165 | isError: false 166 | }; 167 | } 168 | 169 | // No data found for date 170 | return { 171 | content: [{ 172 | type: "text", 173 | text: `No EPIC data found for date ${date || 'latest'} in collection ${collection}` 174 | }], 175 | isError: false 176 | }; 177 | } catch (error: any) { 178 | console.error('Error in EPIC handler:', error); 179 | 180 | if (error.name === 'ZodError') { 181 | return { 182 | content: [{ 183 | type: "text", 184 | text: `Invalid request parameters: ${error.message}` 185 | }], 186 | isError: true 187 | }; 188 | } 189 | 190 | // Return a properly formatted error 191 | return { 192 | content: [{ 193 | type: "text", 194 | text: `Error fetching EPIC data: ${error.message}` 195 | }], 196 | isError: true 197 | }; 198 | } 199 | } 200 | 201 | // Export the handler function directly as default 202 | export default nasaEpicHandler; 203 | -------------------------------------------------------------------------------- /src/handlers/nasa/cmr.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { addResource } from '../../resources'; 4 | 5 | const CMR_API_BASE_URL = 'https://cmr.earthdata.nasa.gov/search'; 6 | 7 | // Define common spatial parameter schemas 8 | const polygonSchema = z.string().describe('Comma-separated list of lon/lat points defining a polygon'); 9 | const bboxSchema = z.string().describe('Bounding box in the format: west,south,east,north'); 10 | const pointSchema = z.string().describe('Point in the format: lon,lat'); 11 | const lineSchema = z.string().describe('Line in the format: lon1,lat1,lon2,lat2,...'); 12 | const circleSchema = z.string().describe('Circle in the format: lon,lat,radius'); 13 | 14 | // Schema for validating CMR request parameters 15 | export const cmrParamsSchema = z.object({ 16 | // Search type - collections or granules 17 | search_type: z.enum(['collections', 'granules']).default('collections'), 18 | 19 | // Basic search parameters 20 | keyword: z.string().optional(), 21 | concept_id: z.string().optional(), 22 | entry_title: z.string().optional(), 23 | short_name: z.string().optional(), 24 | provider: z.string().optional(), 25 | 26 | // Temporal parameters 27 | temporal: z.string().optional().describe('Temporal range in the format: start_date,end_date'), 28 | 29 | // Spatial parameters 30 | polygon: polygonSchema.optional(), 31 | bbox: bboxSchema.optional(), 32 | point: pointSchema.optional(), 33 | line: lineSchema.optional(), 34 | circle: circleSchema.optional(), 35 | 36 | // Platform, instrument, and project 37 | platform: z.string().optional(), 38 | instrument: z.string().optional(), 39 | project: z.string().optional(), 40 | 41 | // Processing level and data format 42 | processing_level_id: z.string().optional(), 43 | granule_data_format: z.string().optional(), 44 | 45 | // Search flags 46 | downloadable: z.boolean().optional(), 47 | browsable: z.boolean().optional(), 48 | online_only: z.boolean().optional(), 49 | 50 | // Facet parameters 51 | include_facets: z.boolean().optional(), 52 | 53 | // Pagination and sorting 54 | limit: z.number().optional().default(10), 55 | page: z.number().optional().default(1), 56 | offset: z.number().optional(), 57 | sort_key: z.string().optional(), 58 | 59 | // Result format 60 | format: z.enum(['json', 'umm_json', 'atom', 'echo10', 'iso19115', 'iso_smap', 'kml']).optional().default('json') 61 | }); 62 | 63 | // Define the request parameter type based on the schema 64 | export type CmrParams = z.infer; 65 | 66 | /** 67 | * Handle requests for NASA's Common Metadata Repository (CMR) API 68 | */ 69 | export async function nasaCmrHandler(params: CmrParams) { 70 | try { 71 | const { 72 | search_type, format, limit, page, offset, sort_key, include_facets, 73 | polygon, bbox, point, line, circle, temporal, 74 | ...otherParams 75 | } = params; 76 | 77 | // Determine the correct format extension for the URL 78 | let formatExtension = format; 79 | if (format === 'json') { 80 | formatExtension = 'json'; 81 | } else if (format === 'umm_json') { 82 | formatExtension = 'umm_json'; 83 | } 84 | 85 | // Determine search endpoint based on search type 86 | const endpoint = `/${search_type}.${formatExtension}`; 87 | 88 | // Construct parameters 89 | const queryParams: Record = { 90 | page_size: limit, 91 | page_num: page, 92 | offset, 93 | sort_key 94 | }; 95 | 96 | // Add other parameters 97 | for (const [key, value] of Object.entries(otherParams)) { 98 | if (value !== undefined) { 99 | queryParams[key] = value; 100 | } 101 | } 102 | 103 | // Add temporal parameter if provided 104 | if (temporal) { 105 | queryParams.temporal = temporal; 106 | } 107 | 108 | // Add spatial parameters if provided 109 | if (polygon) queryParams.polygon = polygon; 110 | if (bbox) queryParams.bbox = bbox; 111 | if (point) queryParams.point = point; 112 | if (line) queryParams.line = line; 113 | if (circle) queryParams.circle = circle; 114 | 115 | // Add facet options if requested 116 | if (include_facets) { 117 | queryParams.include_facets = 'v2'; 118 | } 119 | 120 | // Make the request to CMR directly 121 | const response = await axios({ 122 | url: `${CMR_API_BASE_URL}${endpoint}`, 123 | params: queryParams, 124 | headers: { 125 | 'Client-Id': 'NASA-MCP-Server', 126 | 'Accept': format === 'json' || format === 'umm_json' ? 'application/json' : undefined 127 | }, 128 | timeout: 30000 // 30 second timeout 129 | }); 130 | 131 | // Parse the response based on format 132 | let data; 133 | if (format === 'json' || format === 'umm_json') { 134 | data = response.data; 135 | } else { 136 | // For non-JSON formats, just return the raw text 137 | data = { 138 | raw: response.data, 139 | format: format 140 | }; 141 | } 142 | 143 | // Format the response to match MCP expectations 144 | let summary = ''; 145 | let formattedData; 146 | 147 | if (search_type === 'collections') { 148 | const collectionsCount = 149 | format === 'json' ? (data.feed?.entry?.length || 0) : 150 | format === 'umm_json' ? (data.items?.length || 0) : 151 | 0; 152 | summary = `Found ${collectionsCount} NASA collections`; 153 | formattedData = data; 154 | } else { 155 | const granulesCount = 156 | format === 'json' ? (data.feed?.entry?.length || 0) : 157 | format === 'umm_json' ? (data.items?.length || 0) : 158 | 0; 159 | summary = `Found ${granulesCount} data granules`; 160 | formattedData = data; 161 | } 162 | 163 | // Create a resource ID 164 | const resourceParams = []; 165 | if (params.keyword) resourceParams.push(`keyword=${encodeURIComponent(params.keyword)}`); 166 | if (params.concept_id) resourceParams.push(`concept_id=${params.concept_id}`); 167 | if (temporal) resourceParams.push(`temporal=${encodeURIComponent(temporal)}`); 168 | 169 | const resourceId = `nasa://cmr/${search_type}${resourceParams.length > 0 ? '?' + resourceParams.join('&') : ''}`; 170 | 171 | // Register the response as a resource 172 | addResource(resourceId, { 173 | name: `NASA CMR ${search_type} search${params.keyword ? ` for "${params.keyword}"` : ''}`, 174 | mimeType: 'application/json', 175 | text: JSON.stringify(formattedData, null, 2) 176 | }); 177 | 178 | // If the response includes specific collections or granules, register those too 179 | if (formattedData.feed?.entry && Array.isArray(formattedData.feed.entry)) { 180 | formattedData.feed.entry.forEach((entry: any, index: number) => { 181 | if (index < 5) { // Limit to first 5 entries to avoid too many resources 182 | const entryId = entry.id || entry['concept-id'] || `${search_type}-${index}`; 183 | const entryTitle = entry.title || `NASA ${search_type} Item ${index + 1}`; 184 | 185 | const entryResourceId = `nasa://cmr/${search_type}/item?id=${entryId}`; 186 | 187 | addResource(entryResourceId, { 188 | name: entryTitle, 189 | mimeType: 'application/json', 190 | text: JSON.stringify(entry, null, 2) 191 | }); 192 | } 193 | }); 194 | } 195 | 196 | return { 197 | content: [ 198 | { 199 | type: "text", 200 | text: summary 201 | }, 202 | { 203 | type: "text", 204 | text: JSON.stringify(formattedData, null, 2) 205 | } 206 | ], 207 | isError: false 208 | }; 209 | } catch (error: any) { 210 | console.error('Error in CMR handler:', error); 211 | 212 | // Proper error handling with isError flag 213 | return { 214 | isError: true, 215 | content: [{ 216 | type: "text", 217 | text: `Error searching NASA Common Metadata Repository: ${error.message || 'Unknown error'}` 218 | }] 219 | }; 220 | } 221 | } 222 | 223 | // Export the handler function directly as default 224 | export default nasaCmrHandler; 225 | -------------------------------------------------------------------------------- /src/handlers/nasa/neo.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { nasaApiRequest } from '../../utils/api-client'; 3 | import { addResource } from '../../resources'; 4 | 5 | // Schema for validating NEO request parameters 6 | export const neoParamsSchema = z.object({ 7 | start_date: z.string().optional(), 8 | end_date: z.string().optional(), 9 | asteroid_id: z.string().optional() 10 | }); 11 | 12 | // Define the request parameter type based on the schema 13 | export type NeoParams = z.infer; 14 | 15 | /** 16 | * Handle requests for NASA's Near Earth Object Web Service (NEO WS) 17 | */ 18 | export async function nasaNeoHandler(params: NeoParams) { 19 | try { 20 | // If we're looking for a specific asteroid by ID 21 | if (params.asteroid_id) { 22 | const endpoint = `/neo/rest/v1/neo/${params.asteroid_id}`; 23 | const result = await nasaApiRequest(endpoint, {}); 24 | 25 | // Store the result as a resource 26 | addResource(`nasa://neo/${params.asteroid_id}`, { 27 | name: `Asteroid: ${result.name}`, 28 | mimeType: 'application/json', 29 | text: JSON.stringify(result, null, 2) 30 | }); 31 | 32 | // Return formatted result 33 | return { 34 | content: [ 35 | { 36 | type: "text", 37 | text: formatSingleAsteroidText(result) 38 | } 39 | ], 40 | isError: false 41 | }; 42 | } 43 | 44 | // Default to today if no dates specified 45 | let startDate = params.start_date; 46 | let endDate = params.end_date; 47 | 48 | if (!startDate) { 49 | const today = new Date(); 50 | startDate = today.toISOString().split('T')[0]; 51 | } 52 | 53 | // If no end_date, use start_date (same day) 54 | if (!endDate) { 55 | endDate = startDate; 56 | } 57 | 58 | // API limits feed to 7 days 59 | const maxDays = 7; 60 | const start = new Date(startDate); 61 | const end = new Date(endDate); 62 | const daysDiff = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); 63 | 64 | if (daysDiff > maxDays) { 65 | return { 66 | content: [ 67 | { 68 | type: "text", 69 | text: `Error: Date range too large. NEO feed is limited to ${maxDays} days.` 70 | } 71 | ], 72 | isError: true 73 | }; 74 | } 75 | 76 | // Call the NASA NEO API 77 | const endpoint = `/neo/rest/v1/feed?start_date=${startDate}&end_date=${endDate}`; 78 | const result = await nasaApiRequest(endpoint, {}); 79 | 80 | // Process and format the results 81 | return processNeoFeedResults(result, startDate, endDate); 82 | } catch (error: any) { 83 | console.error('Error in NEO handler:', error); 84 | 85 | return { 86 | content: [ 87 | { 88 | type: "text", 89 | text: `Error retrieving NEO data: ${error.message}` 90 | } 91 | ], 92 | isError: true 93 | }; 94 | } 95 | } 96 | 97 | /** 98 | * Format a single asteroid object into a human-readable text 99 | */ 100 | function formatSingleAsteroidText(asteroid: any): string { 101 | // Format the diameter in km 102 | const minDiameterKm = asteroid.estimated_diameter?.kilometers?.estimated_diameter_min?.toFixed(3) || 'unknown'; 103 | const maxDiameterKm = asteroid.estimated_diameter?.kilometers?.estimated_diameter_max?.toFixed(3) || 'unknown'; 104 | const diameterText = `${minDiameterKm} - ${maxDiameterKm} km`; 105 | 106 | // Create intro text 107 | let text = `# Asteroid: ${asteroid.name}\n\n`; 108 | text += `**NEO Reference ID:** ${asteroid.id}\n`; 109 | text += `**Potentially Hazardous:** ${asteroid.is_potentially_hazardous_asteroid ? '⚠️ YES' : '✓ NO'}\n`; 110 | text += `**Estimated Diameter:** ${diameterText}\n\n`; 111 | 112 | // Add close approach data 113 | const closeApproaches = asteroid.close_approach_data || []; 114 | if (closeApproaches.length > 0) { 115 | text += `## Close Approaches\n\n`; 116 | 117 | // Only show the first 5 close approaches to avoid excessively long text 118 | const displayLimit = 5; 119 | const showCount = Math.min(displayLimit, closeApproaches.length); 120 | 121 | for (let i = 0; i < showCount; i++) { 122 | const ca = closeApproaches[i]; 123 | const date = ca.close_approach_date; 124 | const distance = Number(ca.miss_distance.kilometers).toFixed(3); 125 | const lunarDistance = (Number(ca.miss_distance.lunar) || 0).toFixed(2); 126 | const velocity = Number(ca.relative_velocity.kilometers_per_second).toFixed(2); 127 | 128 | text += `- **Date:** ${date}\n`; 129 | text += ` **Distance:** ${distance} km (${lunarDistance} lunar distances)\n`; 130 | text += ` **Relative Velocity:** ${velocity} km/s\n\n`; 131 | } 132 | 133 | // Add a note if there are more close approaches than we're showing 134 | if (closeApproaches.length > displayLimit) { 135 | text += `\n*...and ${closeApproaches.length - displayLimit} more close approaches*\n`; 136 | } 137 | } 138 | 139 | return text; 140 | } 141 | 142 | /** 143 | * Process and format NEO feed results for a date range 144 | */ 145 | function processNeoFeedResults(feedData: any, startDate: string, endDate: string) { 146 | try { 147 | // Store the feed data as a resource 148 | const resourceId = `neo-feed-${startDate}-${endDate}`; 149 | addResource(`nasa://neo/feed/${resourceId}`, { 150 | name: `NEO Feed: ${startDate} to ${endDate}`, 151 | mimeType: 'application/json', 152 | text: JSON.stringify(feedData, null, 2) 153 | }); 154 | 155 | // Format the data for display 156 | const text = formatNeoFeedText(feedData, startDate, endDate); 157 | 158 | return { 159 | content: [ 160 | { 161 | type: "text", 162 | text: text 163 | } 164 | ], 165 | isError: false 166 | }; 167 | } catch (error: any) { 168 | console.error('Error processing NEO feed results:', error); 169 | return { 170 | content: [ 171 | { 172 | type: "text", 173 | text: `Error processing NEO feed data: ${error.message}` 174 | } 175 | ], 176 | isError: true 177 | }; 178 | } 179 | } 180 | 181 | /** 182 | * Format NEO feed data into a human-readable text 183 | */ 184 | function formatNeoFeedText(feedData: any, startDate: string, endDate: string): string { 185 | // Get the count of NEOs 186 | const neoCount = feedData.element_count || 0; 187 | const dateRangeText = startDate === endDate ? startDate : `${startDate} to ${endDate}`; 188 | 189 | // Start with a summary 190 | let text = `# Near Earth Objects (${dateRangeText})\n\n`; 191 | text += `**Found ${neoCount} near-Earth objects**\n\n`; 192 | 193 | // Get the near earth objects by date 194 | const neosByDate = feedData.near_earth_objects || {}; 195 | 196 | // For each date 197 | Object.keys(neosByDate).sort().forEach(date => { 198 | const objects = neosByDate[date] || []; 199 | text += `## ${date} (${objects.length} objects)\n\n`; 200 | 201 | // Sort by close approach time 202 | objects.sort((a: any, b: any) => { 203 | const timeA = a.close_approach_data?.[0]?.close_approach_date_full || ''; 204 | const timeB = b.close_approach_data?.[0]?.close_approach_date_full || ''; 205 | return timeA.localeCompare(timeB); 206 | }); 207 | 208 | // For each object 209 | objects.forEach((neo: any) => { 210 | const name = neo.name; 211 | const id = neo.id; 212 | const isHazardous = neo.is_potentially_hazardous_asteroid; 213 | const minDiameterKm = neo.estimated_diameter?.kilometers?.estimated_diameter_min?.toFixed(3) || '?'; 214 | const maxDiameterKm = neo.estimated_diameter?.kilometers?.estimated_diameter_max?.toFixed(3) || '?'; 215 | 216 | // Close approach data 217 | const closeApproach = neo.close_approach_data?.[0] || {}; 218 | const approachTime = closeApproach.close_approach_date_full || '?'; 219 | const distanceKm = Number(closeApproach.miss_distance?.kilometers || 0).toFixed(0); 220 | const lunarDistance = Number(closeApproach.miss_distance?.lunar || 0).toFixed(2); 221 | const velocityKmps = Number(closeApproach.relative_velocity?.kilometers_per_second || 0).toFixed(2); 222 | 223 | text += `### ${name} (ID: ${id})\n\n`; 224 | text += `- **Potentially Hazardous:** ${isHazardous ? '⚠️ YES' : '✓ NO'}\n`; 225 | text += `- **Estimated Diameter:** ${minDiameterKm} - ${maxDiameterKm} km\n`; 226 | text += `- **Closest Approach:** ${approachTime}\n`; 227 | text += `- **Miss Distance:** ${distanceKm} km (${lunarDistance} lunar distances)\n`; 228 | text += `- **Relative Velocity:** ${velocityKmps} km/s\n\n`; 229 | }); 230 | }); 231 | 232 | return text; 233 | } 234 | 235 | // Export the handler function directly as default 236 | export default nasaNeoHandler; 237 | -------------------------------------------------------------------------------- /src/handlers/nasa/power.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import axios from 'axios'; 3 | import { addResource } from '../../resources'; 4 | 5 | // Schema for validating POWER request parameters 6 | export const powerParamsSchema = z.object({ 7 | parameters: z.string(), 8 | community: z.enum(['re', 'sb', 'ag', 'RE', 'SB', 'AG']).transform(val => val.toLowerCase()), 9 | format: z.enum(['json', 'csv', 'ascii', 'netcdf']).optional().default('json'), 10 | // Location parameters 11 | latitude: z.number().min(-90).max(90).optional(), 12 | longitude: z.number().min(-180).max(180).optional(), 13 | // Regional parameters (alternative to lat/long) 14 | bbox: z.string().optional(), 15 | // Temporal parameters 16 | start: z.string().optional(), 17 | end: z.string().optional(), 18 | // Climatology parameters 19 | climatology_start: z.string().optional(), 20 | climatology_end: z.string().optional(), 21 | time_standard: z.enum(['utc', 'lst']).optional().default('utc') 22 | }); 23 | 24 | export type PowerParams = z.infer; 25 | 26 | /** 27 | * Format POWER API data into a human-readable text 28 | */ 29 | function formatPowerDataText(responseData: any, params: PowerParams): string { 30 | try { 31 | const properties = responseData.properties || {}; 32 | const parameterData = properties.parameter || {}; 33 | const geometry = responseData.geometry || {}; 34 | const header = responseData.header || {}; 35 | 36 | const requestedParams = params.parameters.split(','); 37 | 38 | let locationStr = 'Global'; 39 | if (geometry.type === 'Point' && geometry.coordinates) { 40 | locationStr = `Point (${geometry.coordinates[1]}, ${geometry.coordinates[0]})`; 41 | } else if (params.bbox) { 42 | locationStr = `Region (${params.bbox})`; 43 | } 44 | 45 | let dateRangeStr = ''; 46 | if (header.start && header.end) { 47 | dateRangeStr = `${header.start} to ${header.end}`; 48 | } else if (params.start && params.end) { 49 | dateRangeStr = `${params.start} to ${params.end}`; 50 | } 51 | 52 | let text = `# NASA POWER Data\n\n`; 53 | text += `**Community:** ${params.community.toUpperCase()}\n`; 54 | text += `**Location:** ${locationStr}\n`; 55 | text += `**Date Range:** ${dateRangeStr || 'N/A'}\n\n`; 56 | 57 | text += `## Parameters\n\n`; 58 | 59 | requestedParams.forEach(paramKey => { 60 | const data = parameterData[paramKey]; 61 | const unit = header.parameter_information?.[paramKey]?.units || 'N/A'; 62 | const longName = header.parameter_information?.[paramKey]?.long_name || paramKey; 63 | 64 | text += `### ${longName} (${paramKey})\n`; 65 | text += `- **Units:** ${unit}\n`; 66 | 67 | if (data && typeof data === 'object') { 68 | const dates = Object.keys(data).sort(); 69 | if (dates.length > 0) { 70 | text += `- **Data:**\n`; 71 | text += '| Date | Value |\n'; 72 | text += '|------------|-------|\n'; 73 | // Show first 10 and last 10 dates to avoid excessive length 74 | const maxEntries = 10; 75 | const totalEntries = dates.length; 76 | let entriesShown = 0; 77 | 78 | for (let i = 0; i < Math.min(maxEntries, totalEntries); i++) { 79 | const date = dates[i]; 80 | const value = data[date] !== undefined ? data[date] : 'N/A'; 81 | text += `| ${date} | ${value} | 82 | `; 83 | entriesShown++; 84 | } 85 | 86 | if (totalEntries > maxEntries * 2) { 87 | text += `| ... | ... |\n`; // Indicate truncation 88 | } 89 | 90 | if (totalEntries > maxEntries) { 91 | const startIndex = Math.max(maxEntries, totalEntries - maxEntries); 92 | for (let i = startIndex; i < totalEntries; i++) { 93 | const date = dates[i]; 94 | const value = data[date] !== undefined ? data[date] : 'N/A'; 95 | text += `| ${date} | ${value} | 96 | `; 97 | entriesShown++; 98 | } 99 | } 100 | text += `\n*(Showing ${entriesShown} of ${totalEntries} daily values)*\n\n`; 101 | } else { 102 | text += `- Data: No data available for this period.\n\n`; 103 | } 104 | } else { 105 | text += `- Data: Not available or invalid format.\n\n`; 106 | } 107 | }); 108 | 109 | return text; 110 | } catch (formatError: any) { 111 | console.error('Error formatting POWER data:', formatError); 112 | return `Error: Failed to format POWER data. Raw data might be available in resources. Error: ${formatError.message}`; 113 | } 114 | } 115 | 116 | /** 117 | * Handle requests for NASA's POWER (Prediction Of Worldwide Energy Resources) API 118 | * Provides solar and meteorological data sets 119 | */ 120 | export async function nasaPowerHandler(params: PowerParams) { 121 | try { 122 | // POWER API base URL 123 | const POWER_API_URL = 'https://power.larc.nasa.gov/api/temporal/daily/point'; 124 | 125 | // Validate and normalize parameters using the schema 126 | const validatedParams = powerParamsSchema.parse(params); 127 | 128 | // Call the NASA POWER API 129 | const response = await axios({ 130 | url: POWER_API_URL, 131 | params: validatedParams, // Use validated & normalized params 132 | method: 'GET', 133 | timeout: 30000 // Increased timeout to 30 seconds 134 | }); 135 | 136 | // Create a resource ID based on key parameters 137 | const resourceParams = []; 138 | if (validatedParams.parameters) resourceParams.push(`parameters=${validatedParams.parameters}`); 139 | if (validatedParams.latitude !== undefined) resourceParams.push(`lat=${validatedParams.latitude}`); 140 | if (validatedParams.longitude !== undefined) resourceParams.push(`lon=${validatedParams.longitude}`); 141 | if (validatedParams.start) resourceParams.push(`start=${validatedParams.start}`); 142 | if (validatedParams.end) resourceParams.push(`end=${validatedParams.end}`); 143 | 144 | const resourceId = `nasa://power/${validatedParams.community}?${resourceParams.join('&')}`; 145 | 146 | // Register the response as a resource 147 | addResource(resourceId, { 148 | name: `NASA POWER ${validatedParams.community.toUpperCase()} Data${validatedParams.latitude !== undefined ? ` at (${validatedParams.latitude}, ${validatedParams.longitude})` : ''}`, 149 | mimeType: validatedParams.format === 'json' ? 'application/json' : 'text/plain', 150 | text: validatedParams.format === 'json' ? JSON.stringify(response.data, null, 2) : response.data 151 | }); 152 | 153 | // Format the data for display 154 | const formattedText = formatPowerDataText(response.data, validatedParams); 155 | 156 | // Return the formatted result 157 | return { 158 | content: [ 159 | { 160 | type: "text", 161 | text: formattedText 162 | } 163 | ], 164 | isError: false 165 | }; 166 | } catch (error: any) { 167 | // Handle Zod validation errors separately 168 | if (error instanceof z.ZodError) { 169 | console.error('Error validating POWER parameters:', error.errors); 170 | return { 171 | isError: true, 172 | content: [{ 173 | type: "text", 174 | text: `Error: Invalid parameters for NASA POWER API. Issues: ${error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join('; ')}` 175 | }] 176 | }; 177 | } 178 | 179 | console.error('Error in POWER handler:', error); 180 | 181 | let errorMessage = 'An unexpected error occurred'; 182 | 183 | if (error.response) { 184 | // The request was made and the server responded with an error status 185 | console.error('Response status:', error.response.status); 186 | console.error('Response headers:', JSON.stringify(error.response.headers)); 187 | console.error('Response data:', JSON.stringify(error.response.data).substring(0, 200)); 188 | 189 | // Check content type before assuming JSON for error response 190 | const contentType = error.response.headers?.['content-type'] || ''; 191 | let errorDetails = 'Unknown error'; 192 | if (contentType.includes('application/json') && error.response.data) { 193 | errorDetails = error.response.data?.message || error.response.data?.errors?.join(', ') || 194 | JSON.stringify(error.response.data).substring(0, 100); 195 | } else if (typeof error.response.data === 'string') { 196 | errorDetails = error.response.data.substring(0, 150); // Show raw text if not JSON 197 | } 198 | 199 | errorMessage = `NASA POWER API error (${error.response.status}): ${errorDetails}`; 200 | } else if (error.request) { 201 | // The request was made but no response was received 202 | console.error('Request details:'); 203 | console.error('- URL:', error.config?.url || 'Not available'); // Use config for URL 204 | console.error('- Params:', error.config?.params || 'Not available'); 205 | console.error('- Method:', error.request.method || 'Not available'); 206 | console.error('- Headers:', error.request._header || 'Not available'); 207 | 208 | errorMessage = `NASA POWER API network error: No response received or request timed out. Check network connectivity and API status. URL: ${error.config?.url}`; 209 | } else { 210 | // Something happened in setting up the request 211 | errorMessage = `NASA POWER API request setup error: ${error.message}`; 212 | } 213 | 214 | return { 215 | isError: true, 216 | content: [{ 217 | type: "text", 218 | text: `Error: ${errorMessage}` 219 | }] 220 | }; 221 | } 222 | } 223 | 224 | // Export the handler function directly as default 225 | export default nasaPowerHandler; 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version](https://img.shields.io/npm/v/%40programcomputer%2Fnasa-mcp-server?link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40programcomputer%2Fnasa-mcp-server)](https://www.npmjs.com/package/@programcomputer/nasa-mcp-server) 2 | 3 | # NASA MCP Server 4 | 5 | A Model Context Protocol (MCP) server for NASA APIs, providing a standardized interface for AI models to interact with NASA's vast array of data sources. This server implements the official Model Context Protocol specification. 6 | 7 | Big thanks to the MCP community for their support and guidance! 8 | 9 | ## Features 10 | 11 | * Access to 20+ NASA data sources through a single, consistent interface 12 | * Standardized data formats optimized for AI consumption 13 | * Automatic parameter validation and error handling 14 | * Rate limit management for NASA API keys 15 | * Comprehensive documentation and examples 16 | * Support for various NASA imagery formats 17 | * Data conversion and formatting for LLM compatibility 18 | * Cross-platform support (Windows, macOS, Linux) 19 | 20 | ## Disclaimer 21 | 22 | **This project is not affiliated with, endorsed by, or related to NASA (National Aeronautics and Space Administration) or any of its subsidiaries or its affiliates.** It is an independent implementation that accesses NASA's publicly available APIs. All NASA data used is publicly available and subject to NASA's data usage policies. 23 | 24 | ## Installation 25 | 26 | ### Running with npx 27 | 28 | ```bash 29 | env NASA_API_KEY=YOUR_API_KEY npx -y @programcomputer/nasa-mcp-server@latest 30 | ``` 31 | 32 | You can also pass the API key as a command line argument: 33 | 34 | ```bash 35 | npx -y @programcomputer/nasa-mcp-server@latest --nasa-api-key=YOUR_API_KEY 36 | ``` 37 | 38 | ### Using SuperGateway for Server-Sent Events (SSE) 39 | 40 | You can use [SuperGateway](https://github.com/supercorp-ai/supergateway) for Server-Sent Events (SSE). 41 | 42 | **The developers of NASA-MCP-server DO NOT ENDORSE the SuperGateway repository. This information is provided for those who wish to implement SSE functionality at their own discretion.** 43 | 44 | ### Manual Installation 45 | 46 | ```bash 47 | # Clone the repository 48 | git clone https://github.com/ProgramComputer/NASA-MCP-server.git 49 | 50 | # Install dependencies 51 | cd NASA-MCP-server 52 | npm install 53 | 54 | # Run with your API key 55 | NASA_API_KEY=YOUR_API_KEY npm start 56 | ``` 57 | 58 | ### Running on Cursor 59 | 60 | Configuring Cursor 🖥️ Note: Requires Cursor version 0.45.6+ 61 | 62 | To configure NASA MCP Server in Cursor: 63 | 64 | Create or edit an `mcp.json` file in your Cursor configuration directory with the following content: 65 | 66 | ```json 67 | { 68 | "mcpServers": { 69 | "nasa-mcp": { 70 | "command": "npx", 71 | "args": ["-y", "@programcomputer/nasa-mcp-server@latest"], 72 | "env": { 73 | "NASA_API_KEY": "your-api-key" 74 | } 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | Replace `your-api-key` with your NASA API key from https://api.nasa.gov/. 81 | 82 | After adding the configuration, restart Cursor to see the new NASA tools. The Composer Agent will automatically use NASA MCP when appropriate for space-related queries. 83 | 84 | ## Environment Variables 85 | 86 | The server can be configured with the following environment variables: 87 | 88 | | Variable | Description | 89 | |----------|-------------| 90 | | `NASA_API_KEY` | Your NASA API key (get at api.nasa.gov) | 91 | 92 | ## Included NASA APIs 93 | 94 | This MCP server integrates the following NASA APIs: 95 | 96 | 1. **NASA Open API** (api.nasa.gov): 97 | - APOD (Astronomy Picture of the Day) 98 | - EPIC (Earth Polychromatic Imaging Camera) 99 | - DONKI (Space Weather Database Of Notifications, Knowledge, Information) 100 | - Insight (Mars Weather Service) 101 | - Mars Rover Photos 102 | - NEO (Near Earth Object Web Service) 103 | - EONET (Earth Observatory Natural Event Tracker) 104 | - TLE (Two-Line Element) 105 | - NASA Image and Video Library 106 | - Exoplanet Archive 107 | - NASA Sounds API (Beta) 108 | - POWER (Prediction Of Worldwide Energy Resources) 109 | 110 | 2. **JPL Solar System Dynamics API** (ssd-api.jpl.nasa.gov): 111 | - SBDB (Small-Body DataBase) 112 | - SBDB Close-Approach Data 113 | - Fireball Data 114 | - Scout API 115 | 116 | 3. **Earth Data APIs**: 117 | - GIBS (Global Imagery Browse Services) 118 | - CMR (Common Metadata Repository) - Enhanced with advanced search capabilities 119 | - EPIC (Earth Polychromatic Imaging Camera) 120 | - FIRMS (Fire Information for Resource Management System) 121 | 122 | ## API Methods 123 | 124 | Each NASA API is exposed through standardized MCP methods: 125 | 126 | ### APOD (Astronomy Picture of the Day) 127 | 128 | ```json 129 | { 130 | "method": "nasa/apod", 131 | "params": { 132 | "date": "2023-01-01", // Optional: YYYY-MM-DD format 133 | "count": 5, // Optional: Return a specified number of random images 134 | "thumbs": true // Optional: Return URL of video thumbnail 135 | } 136 | } 137 | ``` 138 | 139 | ### Mars Rover Photos 140 | 141 | ```json 142 | { 143 | "method": "nasa/mars-rover", 144 | "params": { 145 | "rover": "curiosity", // Required: "curiosity", "opportunity", or "spirit" 146 | "sol": 1000, // Either sol or earth_date is required 147 | "earth_date": "2023-01-01", // YYYY-MM-DD format 148 | "camera": "FHAZ" // Optional: Filter by camera type 149 | } 150 | } 151 | ``` 152 | 153 | ### Near Earth Objects 154 | 155 | ```json 156 | { 157 | "method": "nasa/neo", 158 | "params": { 159 | "start_date": "2023-01-01", // Required: YYYY-MM-DD format 160 | "end_date": "2023-01-07" // Required: YYYY-MM-DD format (max 7 days from start) 161 | } 162 | } 163 | ``` 164 | 165 | ### GIBS (Global Imagery Browse Services) 166 | 167 | ```json 168 | { 169 | "method": "nasa/gibs", 170 | "params": { 171 | "layer": "MODIS_Terra_CorrectedReflectance_TrueColor", // Required: Layer ID 172 | "date": "2023-01-01", // Required: YYYY-MM-DD format 173 | "format": "png" // Optional: "png" or "jpg" 174 | } 175 | } 176 | ``` 177 | 178 | ### POWER (Prediction Of Worldwide Energy Resources) 179 | 180 | ```json 181 | { 182 | "method": "nasa/power", 183 | "params": { 184 | "parameters": "T2M,PRECTOTCORR,WS10M", // Required: Comma-separated list 185 | "community": "re", // Required: Community identifier 186 | "latitude": 40.7128, // Required: Latitude 187 | "longitude": -74.0060, // Required: Longitude 188 | "start": "20220101", // Required: Start date (YYYYMMDD) 189 | "end": "20220107" // Required: End date (YYYYMMDD) 190 | } 191 | } 192 | ``` 193 | 194 | For complete documentation of all available methods and parameters, see the API reference in the `/docs` directory. 195 | 196 | ## Logging System 197 | 198 | The server includes comprehensive logging: 199 | 200 | * Operation status and progress 201 | * Performance metrics 202 | * Rate limit tracking 203 | * Error conditions 204 | * Request validation 205 | 206 | Example log messages: 207 | 208 | ``` 209 | [INFO] NASA MCP Server initialized successfully 210 | [INFO] Processing APOD request for date: 2023-01-01 211 | [INFO] Fetching Mars Rover data for Curiosity, sol 1000 212 | [WARNING] Rate limit threshold reached (80%) 213 | [ERROR] Invalid parameter: 'date' must be in YYYY-MM-DD format 214 | ``` 215 | 216 | ## Security Considerations 217 | 218 | This MCP server implements security best practices following the Model Context Protocol specifications: 219 | 220 | * Input validation and sanitization using Zod schemas 221 | * No execution of arbitrary code 222 | * Protection against command injection 223 | * Proper error handling to prevent information leakage 224 | * Rate limiting and timeout controls for API requests 225 | * No persistent state that could be exploited across sessions 226 | 227 | ## Development 228 | 229 | ```bash 230 | # Clone the repository 231 | git clone https://github.com/ProgramComputer/NASA-MCP-server.git 232 | 233 | # Install dependencies 234 | npm install 235 | 236 | # Copy the example environment file and update with your API keys 237 | cp .env.example .env 238 | 239 | # Build the TypeScript code 240 | npm run build 241 | 242 | # Start the development server 243 | npm run dev 244 | 245 | # Run tests 246 | npm test 247 | ``` 248 | 249 | ## Testing with MCP Inspector 250 | 251 | The NASA MCP Server includes a script to help you test the APIs using the MCP Inspector: 252 | 253 | ```bash 254 | # Run the provided test script 255 | ./scripts/test-with-inspector.sh 256 | ``` 257 | 258 | This will: 259 | 1. Build the project to ensure the latest changes are included 260 | 2. Start the MCP Inspector with the NASA MCP server running 261 | 3. Allow you to interactively test all the NASA APIs 262 | 263 | ### Example Test Requests 264 | 265 | The repository includes example test requests for each API that you can copy and paste into the MCP Inspector: 266 | 267 | ```bash 268 | # View the example test requests 269 | cat docs/inspector-test-examples.md 270 | ``` 271 | 272 | For detailed examples, see the [Inspector Test Examples](docs/inspector-test-examples.md) document. 273 | 274 | ## MCP Client Usage 275 | 276 | This server follows the official Model Context Protocol. Here's an example of how to use it with the MCP SDK: 277 | 278 | ```typescript 279 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 280 | import { HttpClientTransport } from "@modelcontextprotocol/sdk/client/http.js"; 281 | 282 | const transport = new HttpClientTransport({ 283 | url: "http://localhost:3000", 284 | }); 285 | 286 | const client = new Client({ 287 | name: "mcp-client", 288 | version: "1.0.0", 289 | }); 290 | 291 | await client.connect(transport); 292 | 293 | // Example: Get today's Astronomy Picture of the Day 294 | const apodResult = await client.request({ 295 | method: "nasa/apod", 296 | params: {} 297 | }); 298 | 299 | // Example: Get Mars Rover photos 300 | const marsRoverResult = await client.request({ 301 | method: "nasa/mars-rover", 302 | params: { rover: "curiosity", sol: 1000 } 303 | }); 304 | 305 | // Example: Search for Near Earth Objects 306 | const neoResults = await client.request({ 307 | method: "nasa/neo", 308 | params: { 309 | start_date: '2023-01-01', 310 | end_date: '2023-01-07' 311 | } 312 | }); 313 | 314 | // Example: Get satellite imagery from GIBS 315 | const satelliteImage = await client.request({ 316 | method: "nasa/gibs", 317 | params: { 318 | layer: 'MODIS_Terra_CorrectedReflectance_TrueColor', 319 | date: '2023-01-01' 320 | } 321 | }); 322 | 323 | // Example: Use the new POWER API 324 | const powerData = await client.request({ 325 | method: "nasa/power", 326 | params: { 327 | parameters: "T2M,PRECTOTCORR,WS10M", 328 | community: "re", 329 | latitude: 40.7128, 330 | longitude: -74.0060, 331 | start: "20220101", 332 | end: "20220107" 333 | } 334 | }); 335 | ``` 336 | 337 | ## Contributing 338 | 339 | 1. Fork the repository 340 | 2. Create your feature branch 341 | 3. Run tests: `npm test` 342 | 4. Submit a pull request 343 | 344 | ## License 345 | 346 | ISC License - see LICENSE file for details 347 | -------------------------------------------------------------------------------- /src/utils/manifest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP Manifest generator 3 | * This file defines the MCP manifest that describes the available APIs 4 | */ 5 | 6 | export interface MCPManifest { 7 | schema_version: string; 8 | server_name: string; 9 | server_version: string; 10 | description: string; 11 | endpoints: MCPEndpoint[]; 12 | } 13 | 14 | export interface MCPEndpoint { 15 | name: string; 16 | description: string; 17 | endpoint: string; 18 | schema: any; 19 | } 20 | 21 | export function getManifest(): MCPManifest { 22 | return { 23 | schema_version: '0.1.0', 24 | server_name: process.env.MCP_SERVER_NAME || 'NASA MCP Server', 25 | server_version: '1.0.0', 26 | description: process.env.MCP_DESCRIPTION || 'Model Context Protocol server for NASA APIs', 27 | endpoints: [ 28 | // NASA APOD API 29 | { 30 | name: 'apod', 31 | description: 'Get the Astronomy Picture of the Day', 32 | endpoint: '/nasa/apod', 33 | schema: { 34 | type: 'object', 35 | properties: { 36 | date: { 37 | type: 'string', 38 | description: 'The date of the APOD image to retrieve (YYYY-MM-DD format)' 39 | }, 40 | hd: { 41 | type: 'boolean', 42 | description: 'Whether to return the high definition image' 43 | }, 44 | count: { 45 | type: 'integer', 46 | description: 'If specified, returns that number of random APODs' 47 | }, 48 | start_date: { 49 | type: 'string', 50 | description: 'Start date for a date range (YYYY-MM-DD format)' 51 | }, 52 | end_date: { 53 | type: 'string', 54 | description: 'End date for a date range (YYYY-MM-DD format)' 55 | }, 56 | thumbs: { 57 | type: 'boolean', 58 | description: 'Return thumbnail URLs if the APOD is a video' 59 | } 60 | } 61 | } 62 | }, 63 | 64 | // NASA EPIC API 65 | { 66 | name: 'epic', 67 | description: 'Access Earth Polychromatic Imaging Camera data', 68 | endpoint: '/nasa/epic', 69 | schema: { 70 | type: 'object', 71 | properties: { 72 | collection: { 73 | type: 'string', 74 | description: 'Image collection (natural or enhanced)', 75 | enum: ['natural', 'enhanced'] 76 | }, 77 | date: { 78 | type: 'string', 79 | description: 'Date of the image (YYYY-MM-DD)' 80 | } 81 | } 82 | } 83 | }, 84 | 85 | // NASA NEO API 86 | { 87 | name: 'searchNEO', 88 | description: 'Search for near-Earth objects', 89 | endpoint: '/nasa/neo', 90 | schema: { 91 | type: 'object', 92 | properties: { 93 | startDate: { 94 | type: 'string', 95 | description: 'Start date for NEO search range (YYYY-MM-DD format)' 96 | }, 97 | endDate: { 98 | type: 'string', 99 | description: 'End date for NEO search range (YYYY-MM-DD format)' 100 | }, 101 | asteroidId: { 102 | type: 'string', 103 | description: 'Specific asteroid ID to look up' 104 | } 105 | }, 106 | required: ['startDate', 'endDate'] 107 | } 108 | }, 109 | 110 | // NASA EONET API 111 | { 112 | name: 'eonet', 113 | description: 'Access the Earth Observatory Natural Event Tracker', 114 | endpoint: '/nasa/eonet', 115 | schema: { 116 | type: 'object', 117 | properties: { 118 | category: { 119 | type: 'string', 120 | description: 'Event category (e.g. "wildfires", "seaLakeIce", etc.)' 121 | }, 122 | days: { 123 | type: 'integer', 124 | description: 'Number of days to look back for events' 125 | }, 126 | source: { 127 | type: 'string', 128 | description: 'Source of event data' 129 | }, 130 | status: { 131 | type: 'string', 132 | enum: ['open', 'closed', 'all'], 133 | description: 'Event status' 134 | }, 135 | limit: { 136 | type: 'integer', 137 | description: 'Maximum number of events to return' 138 | } 139 | } 140 | } 141 | }, 142 | 143 | // Mars Rover API 144 | { 145 | name: 'marsRover', 146 | description: 'Access Mars Rover photos', 147 | endpoint: '/nasa/mars_rover', 148 | schema: { 149 | type: 'object', 150 | properties: { 151 | rover: { 152 | type: 'string', 153 | description: 'Name of the rover (curiosity, opportunity, spirit, perseverance)', 154 | enum: ['curiosity', 'opportunity', 'spirit', 'perseverance'] 155 | }, 156 | sol: { 157 | type: 'integer', 158 | description: 'Martian sol (day) of the photos' 159 | }, 160 | earth_date: { 161 | type: 'string', 162 | description: 'Earth date of the photos (YYYY-MM-DD format)' 163 | }, 164 | camera: { 165 | type: 'string', 166 | description: 'Rover camera type' 167 | }, 168 | page: { 169 | type: 'integer', 170 | description: 'Page number for pagination' 171 | } 172 | }, 173 | required: ['rover'] 174 | } 175 | }, 176 | 177 | // DONKI API 178 | { 179 | name: 'donki', 180 | description: 'Access the Space Weather Database Of Notifications, Knowledge, Information (DONKI)', 181 | endpoint: '/nasa/donki', 182 | schema: { 183 | type: 'object', 184 | properties: { 185 | type: { 186 | type: 'string', 187 | enum: ['cme', 'cmea', 'gst', 'ips', 'flr', 'sep', 'mpc', 'rbe', 'hss', 'wsa', 'notifications'], 188 | description: 'Type of space weather event' 189 | }, 190 | startDate: { 191 | type: 'string', 192 | description: 'Start date for the search (YYYY-MM-DD format)' 193 | }, 194 | endDate: { 195 | type: 'string', 196 | description: 'End date for the search (YYYY-MM-DD format)' 197 | } 198 | }, 199 | required: ['type'] 200 | } 201 | }, 202 | 203 | // New APIs 204 | 205 | // GIBS API 206 | { 207 | name: 'gibs', 208 | description: 'Access Global Imagery Browse Services (GIBS) satellite imagery', 209 | endpoint: '/nasa/gibs', 210 | schema: { 211 | type: 'object', 212 | properties: { 213 | date: { 214 | type: 'string', 215 | description: 'The date of imagery to retrieve (YYYY-MM-DD format)' 216 | }, 217 | layer: { 218 | type: 'string', 219 | description: 'The GIBS imagery layer to retrieve' 220 | }, 221 | resolution: { 222 | type: 'string', 223 | description: 'Resolution of the imagery' 224 | }, 225 | format: { 226 | type: 'string', 227 | enum: ['image/png', 'image/jpeg', 'image/tiff'], 228 | description: 'Format of the imagery' 229 | }, 230 | tileMatrixSet: { 231 | type: 'string', 232 | description: 'The tile matrix set to use' 233 | }, 234 | bbox: { 235 | type: 'string', 236 | description: 'Bounding box for the imagery (minx,miny,maxx,maxy)' 237 | } 238 | }, 239 | required: ['layer'] 240 | } 241 | }, 242 | 243 | // CMR API 244 | { 245 | name: 'cmr', 246 | description: 'Search NASA\'s Common Metadata Repository for Earth science data', 247 | endpoint: '/nasa/cmr', 248 | schema: { 249 | type: 'object', 250 | properties: { 251 | keyword: { 252 | type: 'string', 253 | description: 'Keyword to search for' 254 | }, 255 | concept_id: { 256 | type: 'string', 257 | description: 'Concept ID to search for' 258 | }, 259 | collection_id: { 260 | type: 'string', 261 | description: 'Collection ID to search for' 262 | }, 263 | temporal: { 264 | type: 'string', 265 | description: 'Temporal range to search for (e.g. "2000-01-01T00:00:00Z,2020-01-01T00:00:00Z")' 266 | }, 267 | point: { 268 | type: 'string', 269 | description: 'Point to search for (e.g. "lon,lat")' 270 | }, 271 | bounding_box: { 272 | type: 'string', 273 | description: 'Bounding box to search for (e.g. "minlon,minlat,maxlon,maxlat")' 274 | }, 275 | limit: { 276 | type: 'integer', 277 | description: 'Maximum number of results to return' 278 | }, 279 | offset: { 280 | type: 'integer', 281 | description: 'Offset for pagination' 282 | }, 283 | provider: { 284 | type: 'string', 285 | description: 'Provider to search for' 286 | }, 287 | sort_key: { 288 | type: 'string', 289 | description: 'Field to sort by' 290 | } 291 | } 292 | } 293 | }, 294 | 295 | // FIRMS API 296 | { 297 | name: 'firms', 298 | description: 'Access Fire Information for Resource Management System (FIRMS) data', 299 | endpoint: '/nasa/firms', 300 | schema: { 301 | type: 'object', 302 | properties: { 303 | source: { 304 | type: 'string', 305 | enum: ['VIIRS_NOAA20_NRT', 'VIIRS_SNPP_NRT', 'MODIS_NRT'], 306 | description: 'Source of fire data' 307 | }, 308 | day_range: { 309 | type: 'integer', 310 | description: 'Number of days of data to retrieve (1-10)' 311 | }, 312 | latitude: { 313 | type: 'number', 314 | description: 'Latitude for point-based search' 315 | }, 316 | longitude: { 317 | type: 'number', 318 | description: 'Longitude for point-based search' 319 | }, 320 | radius: { 321 | type: 'number', 322 | description: 'Radius in km for point-based search' 323 | }, 324 | area: { 325 | type: 'string', 326 | description: 'Area name for area-based search' 327 | }, 328 | format: { 329 | type: 'string', 330 | enum: ['csv', 'json', 'geojson'], 331 | description: 'Output format' 332 | } 333 | } 334 | } 335 | }, 336 | 337 | // NASA Image and Video Library API 338 | { 339 | name: 'images', 340 | description: 'Search NASA\'s Image and Video Library', 341 | endpoint: '/nasa/images', 342 | schema: { 343 | type: 'object', 344 | properties: { 345 | q: { 346 | type: 'string', 347 | description: 'Free text search terms to find assets' 348 | }, 349 | center: { 350 | type: 'string', 351 | description: 'NASA center to search for' 352 | }, 353 | media_type: { 354 | type: 'string', 355 | enum: ['image', 'audio', 'video'], 356 | description: 'Media type to search for' 357 | }, 358 | nasa_id: { 359 | type: 'string', 360 | description: 'Specific NASA ID to retrieve' 361 | }, 362 | keywords: { 363 | type: 'array', 364 | items: { 365 | type: 'string' 366 | }, 367 | description: 'Keywords to search for' 368 | }, 369 | year_start: { 370 | type: 'integer', 371 | description: 'Start year for date range filter' 372 | }, 373 | year_end: { 374 | type: 'integer', 375 | description: 'End year for date range filter' 376 | }, 377 | page: { 378 | type: 'integer', 379 | description: 'Page number for pagination' 380 | }, 381 | page_size: { 382 | type: 'integer', 383 | description: 'Number of items per page' 384 | } 385 | } 386 | } 387 | }, 388 | 389 | // Exoplanet Archive API 390 | { 391 | name: 'exoplanet', 392 | description: 'Access NASA\'s Exoplanet Archive', 393 | endpoint: '/nasa/exoplanet', 394 | schema: { 395 | type: 'object', 396 | properties: { 397 | table: { 398 | type: 'string', 399 | enum: ['ps', 'pscomppars', 'exomultpars'], 400 | description: 'Table to query (ps: Planetary Systems, pscomppars: Planetary Systems Composite Parameters, exomultpars: Extended Planet Parameters)' 401 | }, 402 | select: { 403 | type: 'string', 404 | description: 'Columns to select (comma-separated, or * for all)' 405 | }, 406 | where: { 407 | type: 'string', 408 | description: 'WHERE clause for filtering results' 409 | }, 410 | order: { 411 | type: 'string', 412 | description: 'ORDER BY clause for sorting results' 413 | }, 414 | format: { 415 | type: 'string', 416 | enum: ['json', 'csv', 'xml'], 417 | description: 'Output format' 418 | }, 419 | limit: { 420 | type: 'integer', 421 | description: 'Maximum number of results to return' 422 | } 423 | } 424 | } 425 | }, 426 | 427 | // JPL SBDB API 428 | { 429 | name: 'sbdb', 430 | description: 'Search the Small-Body Database (SBDB)', 431 | endpoint: '/jpl/sbdb', 432 | schema: { 433 | type: 'object', 434 | properties: { 435 | searchName: { 436 | type: 'string', 437 | description: 'The name of the small body to search for' 438 | }, 439 | spkId: { 440 | type: 'string', 441 | description: 'The SPK-ID of the small body' 442 | }, 443 | designation: { 444 | type: 'string', 445 | description: 'Designation to search for' 446 | }, 447 | fullPrecision: { 448 | type: 'boolean', 449 | description: 'Return full-precision data' 450 | } 451 | } 452 | } 453 | }, 454 | 455 | // JPL Fireball API 456 | { 457 | name: 'fireball', 458 | description: 'Search JPL fireball data', 459 | endpoint: '/jpl/fireball', 460 | schema: { 461 | type: 'object', 462 | properties: { 463 | date_min: { 464 | type: 'string', 465 | description: 'Minimum date (YYYY-MM-DD format)' 466 | }, 467 | date_max: { 468 | type: 'string', 469 | description: 'Maximum date (YYYY-MM-DD format)' 470 | }, 471 | energy_min: { 472 | type: 'number', 473 | description: 'Minimum energy (kilotons)' 474 | }, 475 | req_loc: { 476 | type: 'boolean', 477 | description: 'Require location data to be included' 478 | } 479 | } 480 | } 481 | }, 482 | 483 | // JPL Scout API 484 | { 485 | name: 'scout', 486 | description: 'Access the JPL Scout API for recent/current asteroid hazard assessment', 487 | endpoint: '/jpl/scout', 488 | schema: { 489 | type: 'object', 490 | properties: { 491 | orbit_id: { 492 | type: 'string', 493 | description: 'Orbit ID for specific asteroid' 494 | }, 495 | tdes: { 496 | type: 'string', 497 | description: 'Temporary designation' 498 | } 499 | } 500 | } 501 | } 502 | ] 503 | }; 504 | } --------------------------------------------------------------------------------