Skip to content

Commit f819e26

Browse files
1.0.10
1 parent 36dfc5f commit f819e26

File tree

11 files changed

+239
-154
lines changed

11 files changed

+239
-154
lines changed

README.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ Big thanks to the MCP community for their support and guidance!
2424
### Running with npx
2525

2626
```bash
27-
env NASA_API_KEY=YOUR_API_KEY npx -y @programcomputer/nasa-mcp-server
27+
env NASA_API_KEY=YOUR_API_KEY npx -y @programcomputer/nasa-mcp-server@latest
2828
```
2929

3030
You can also pass the API key as a command line argument:
3131

3232
```bash
33-
npx -y @programcomputer/nasa-mcp-server --nasa-api-key=YOUR_API_KEY
33+
npx -y @programcomputer/nasa-mcp-server@latest --nasa-api-key=YOUR_API_KEY
3434
```
3535

3636
### Using SuperGateway for Server-Sent Events (SSE)
@@ -66,7 +66,7 @@ Create or edit an `mcp.json` file in your Cursor configuration directory with th
6666
"mcpServers": {
6767
"nasa-mcp": {
6868
"command": "npx",
69-
"args": ["-y", "@programcomputer/nasa-mcp-server"],
69+
"args": ["-y", "@programcomputer/nasa-mcp-server@latest"],
7070
"env": {
7171
"NASA_API_KEY": "your-api-key"
7272
}
@@ -83,13 +83,9 @@ After adding the configuration, restart Cursor to see the new NASA tools. The Co
8383

8484
The server can be configured with the following environment variables:
8585

86-
| Variable | Description | Default |
87-
|----------|-------------|---------|
88-
| `NASA_API_KEY` | Your NASA API key (get at api.nasa.gov) | `DEMO_KEY` (limited usage) |
89-
| `PORT` | Port to run the server on | `3000` |
90-
| `LOG_LEVEL` | Logging level (debug, info, warn, error) | `info` |
91-
| `CACHE_DURATION` | Cache duration in seconds | `3600` (1 hour) |
92-
| `RATE_LIMIT` | Maximum requests per hour | Based on API key |
86+
| Variable | Description |
87+
|----------|-------------|
88+
| `NASA_API_KEY` | Your NASA API key (get at api.nasa.gov) |
9389

9490
## Included NASA APIs
9591

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@programcomputer/nasa-mcp-server",
3-
"version": "1.0.9",
3+
"version": "1.0.10",
44
"description": "Model Context Protocol (MCP) server for NASA APIs",
55
"main": "dist/index.js",
66
"files": [
@@ -10,7 +10,7 @@
1010
"nasa-mcp-server": "dist/index.js"
1111
},
1212
"scripts": {
13-
"build": "tsc && shx chmod +x dist/*.js",
13+
"build": "npm install && tsc && shx chmod +x dist/*.js",
1414
"start": "node dist/index.js",
1515
"start:sse": "node dist/sse-server.js",
1616
"dev": "tsx watch src/index.ts",

src/handlers/jpl/fireball.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@ export async function jplFireballHandler(params: FireballParams) {
3737
const response = await axios.get(url, { params });
3838

3939
return {
40-
content: [{
41-
type: "text",
42-
text: `Retrieved ${response.data.count || 0} fireball events.`
43-
}],
40+
content: [
41+
{
42+
type: "text",
43+
text: `Retrieved ${response.data.count || 0} fireball events.`
44+
},
45+
{
46+
type: "text",
47+
text: JSON.stringify(response.data, null, 2)
48+
}
49+
],
4450
isError: false
4551
};
4652
} catch (error: any) {

src/handlers/jpl/sbdb.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,16 @@ export async function jplSbdbHandler(params: SbdbParams) {
6363

6464
// Return the response
6565
return {
66-
content: [{
67-
type: "text",
68-
text: `Retrieved data for small body "${params.sstr}".`
69-
}],
66+
content: [
67+
{
68+
type: "text",
69+
text: `Retrieved data for small body "${params.sstr}".`
70+
},
71+
{
72+
type: "text",
73+
text: JSON.stringify(response.data, null, 2)
74+
}
75+
],
7076
isError: false
7177
};
7278
} catch (error: any) {

src/handlers/nasa/donki.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,19 @@ export async function nasaDonkiHandler(params: DonkiParams) {
2525
notifications: '/DONKI/notifications'
2626
};
2727

28-
const endpoint = typeEndpoints[type];
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+
2941
const queryParams: Record<string, any> = {};
3042

3143
// Add date parameters if provided
@@ -48,12 +60,18 @@ export async function nasaDonkiHandler(params: DonkiParams) {
4860
text: JSON.stringify(result, null, 2)
4961
});
5062

51-
// Return the result
63+
// Return the confirmation message and the actual data
5264
return {
53-
content: [{
54-
type: "text",
55-
text: `Retrieved DONKI ${type.toUpperCase()} space weather data${startDate ? ` from ${startDate}` : ''}${endDate ? ` to ${endDate}` : ''}.`
56-
}],
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+
],
5775
isError: false
5876
};
5977
} catch (error: any) {

src/handlers/nasa/power.ts

Lines changed: 161 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { addResource } from '../../index';
55
// Schema for validating POWER request parameters
66
export const powerParamsSchema = z.object({
77
parameters: z.string(),
8-
community: z.enum(['re', 'sb', 'ag']),
8+
community: z.enum(['re', 'sb', 'ag', 'RE', 'SB', 'AG']).transform(val => val.toLowerCase()),
99
format: z.enum(['json', 'csv', 'ascii', 'netcdf']).optional().default('json'),
1010
// Location parameters
1111
latitude: z.number().min(-90).max(90).optional(),
@@ -23,6 +23,96 @@ export const powerParamsSchema = z.object({
2323

2424
export type PowerParams = z.infer<typeof powerParamsSchema>;
2525

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+
26116
/**
27117
* Handle requests for NASA's POWER (Prediction Of Worldwide Energy Resources) API
28118
* Provides solar and meteorological data sets
@@ -32,62 +122,100 @@ export async function nasaPowerHandler(params: PowerParams) {
32122
// POWER API base URL
33123
const POWER_API_URL = 'https://power.larc.nasa.gov/api/temporal/daily/point';
34124

125+
// Validate and normalize parameters using the schema
126+
const validatedParams = powerParamsSchema.parse(params);
127+
35128
// Call the NASA POWER API
36129
const response = await axios({
37130
url: POWER_API_URL,
38-
params: params,
39-
method: 'GET'
131+
params: validatedParams, // Use validated & normalized params
132+
method: 'GET',
133+
timeout: 30000 // Increased timeout to 30 seconds
40134
});
41135

42136
// Create a resource ID based on key parameters
43137
const resourceParams = [];
44-
if (params.parameters) resourceParams.push(`parameters=${params.parameters}`);
45-
if (params.latitude !== undefined) resourceParams.push(`lat=${params.latitude}`);
46-
if (params.longitude !== undefined) resourceParams.push(`lon=${params.longitude}`);
47-
if (params.start) resourceParams.push(`start=${params.start}`);
48-
if (params.end) resourceParams.push(`end=${params.end}`);
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}`);
49143

50-
const resourceId = `nasa://power/${params.community}?${resourceParams.join('&')}`;
144+
const resourceId = `nasa://power/${validatedParams.community}?${resourceParams.join('&')}`;
51145

52146
// Register the response as a resource
53147
addResource(resourceId, {
54-
name: `NASA POWER ${params.community.toUpperCase()} Data${params.latitude !== undefined ? ` at (${params.latitude}, ${params.longitude})` : ''}`,
55-
mimeType: params.format === 'json' ? 'application/json' : 'text/plain',
56-
text: params.format === 'json' ? JSON.stringify(response.data, null, 2) : response.data
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
57151
});
58152

59-
// Extract metadata for more informative response
60-
let paramNames = '';
61-
if (params.parameters) {
62-
paramNames = params.parameters.split(',').join(', ');
63-
}
64-
65-
let locationStr = '';
66-
if (params.latitude !== undefined && params.longitude !== undefined) {
67-
locationStr = `(${params.latitude}, ${params.longitude})`;
68-
}
69-
70-
let dateRangeStr = '';
71-
if (params.start && params.end) {
72-
dateRangeStr = `from ${params.start} to ${params.end}`;
73-
}
153+
// Format the data for display
154+
const formattedText = formatPowerDataText(response.data, validatedParams);
74155

75-
// Return the result
156+
// Return the formatted result
76157
return {
77-
content: [{
78-
type: "text",
79-
text: `Retrieved POWER ${params.community.toUpperCase()} data${locationStr ? ` for location ${locationStr}` : ''}${dateRangeStr ? ` ${dateRangeStr}` : ''}${paramNames ? ` with parameters: ${paramNames}` : ''}.`
80-
}],
158+
content: [
159+
{
160+
type: "text",
161+
text: formattedText
162+
}
163+
],
81164
isError: false
82165
};
83166
} 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+
84179
console.error('Error in POWER handler:', error);
85180

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+
86214
return {
87215
isError: true,
88216
content: [{
89217
type: "text",
90-
text: `Error: ${error.message || 'An unexpected error occurred'}`
218+
text: `Error: ${errorMessage}`
91219
}]
92220
};
93221
}

0 commit comments

Comments
 (0)