Skip to content

Commit 64a49fa

Browse files
Add new NASA Earth imagery API and JPL tools for periodic orbits and scout
1 parent f79317b commit 64a49fa

File tree

10 files changed

+839
-104
lines changed

10 files changed

+839
-104
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
},
4040
"dependencies": {
4141
"@anthropic-ai/sdk": "",
42-
"@modelcontextprotocol/sdk": "^1.7.0",
42+
"@modelcontextprotocol/sdk": "^1.9.0",
4343
"axios": "",
4444
"cors": "",
4545
"dotenv": "",

src/handlers/jpl/horizons_file.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import axios from 'axios';
2+
import FormData from 'form-data'; // Need form-data for multipart POST
3+
import { addResource } from '../../index';
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<string, any>) {
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;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import axios from 'axios';
2+
import { addResource } from '../../index.js';
3+
4+
// Define expected parameters based on documentation
5+
// Required: sys, family
6+
// Optional: libr, branch, periodmin, periodmax, periodunits, jacobimin, jacobimax, stabmin, stabmax
7+
interface PeriodicOrbitParams {
8+
sys: string;
9+
family: string;
10+
libr?: number;
11+
branch?: string;
12+
periodmin?: number;
13+
periodmax?: number;
14+
periodunits?: string;
15+
jacobimin?: number;
16+
jacobimax?: number;
17+
stabmin?: number;
18+
stabmax?: number;
19+
}
20+
21+
/**
22+
* Handler for JPL Three-Body Periodic Orbits API
23+
*
24+
* Fetches data on periodic orbits in specified three-body systems.
25+
*
26+
* @param args Request parameters conforming to PeriodicOrbitParams
27+
* @returns API response
28+
*/
29+
export async function periodicOrbitsHandler(args: PeriodicOrbitParams) {
30+
try {
31+
// Validate required parameters
32+
if (!args.sys || !args.family) {
33+
throw new Error('Missing required parameters: sys and family must be provided.');
34+
}
35+
36+
// Base URL for the Periodic Orbits API
37+
const baseUrl = 'https://ssd-api.jpl.nasa.gov/periodic_orbits.api';
38+
39+
// Make the API request using GET with parameters
40+
const response = await axios.get(baseUrl, { params: args });
41+
const data = response.data;
42+
43+
// Create a resource URI
44+
// Example: jpl://periodic-orbits?sys=earth-moon&family=halo&libr=1&branch=N
45+
let resourceUri = `jpl://periodic-orbits?sys=${encodeURIComponent(args.sys)}&family=${encodeURIComponent(args.family)}`;
46+
let resourceName = `Periodic Orbits: ${args.sys} / ${args.family}`;
47+
if (args.libr) {
48+
resourceUri += `&libr=${args.libr}`;
49+
resourceName += ` / L${args.libr}`;
50+
}
51+
if (args.branch) {
52+
resourceUri += `&branch=${encodeURIComponent(args.branch)}`;
53+
resourceName += ` / Branch ${args.branch}`;
54+
}
55+
// Potentially add filter params to URI/Name if needed for uniqueness
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 for MCP
65+
return {
66+
content: [{
67+
type: "text",
68+
text: JSON.stringify(data, null, 2)
69+
}]
70+
};
71+
} catch (error: any) {
72+
let errorMessage = `Error accessing JPL Periodic Orbits API: ${error.message}`;
73+
if (error.response) {
74+
// Include more detail from the API response if available
75+
errorMessage += `\nStatus: ${error.response.status}\nData: ${JSON.stringify(error.response.data)}`;
76+
}
77+
return {
78+
content: [{
79+
type: "text",
80+
text: errorMessage
81+
}],
82+
isError: true
83+
};
84+
}
85+
}
86+
87+
// Export default for dynamic imports in index.ts
88+
export default periodicOrbitsHandler;

0 commit comments

Comments
 (0)