Skip to main content
Glama
Rkm1999

Celestial Position MCP Server

by Rkm1999

getStarHoppingPath

Calculate a step-by-step star hopping path from a bright start star to a target celestial object, ensuring each hop fits within the specified Field of View (FOV) and magnitude constraints.

Instructions

Calculates a star hopping path from a bright start star to a target celestial object. Each hop is within the specified Field of View (FOV).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
fovDegreesYesThe Field of View (FOV) of the user's equipment in degrees.
initialSearchRadiusDegreesYesThe angular radius around the target object to search for a suitable bright starting star. Default: 20.0 degrees.
maxHopMagnitudeYesThe maximum (dimmest) stellar magnitude for stars in the hopping path. Default: 8.0.
startStarMagnitudeThresholdYesThe maximum (dimmest) magnitude for a star to be a good, bright "starting star." Default: 3.5.
targetObjectNameYesThe name or catalog identifier of the celestial object to find (e.g., "M13", "Andromeda Galaxy", "Mars").

Implementation Reference

  • Tool class definition and name registration as 'getStarHoppingPath'.
    class StarHoppingTool extends MCPTool<StarHoppingInput> { name = 'getStarHoppingPath'; description = 'Calculates a star hopping path from a bright start star to a target celestial object. Each hop is within the specified Field of View (FOV).';
  • Zod-based input schema defining parameters like targetObjectName, fovDegrees, maxHopMagnitude, etc.
    protected schema = { targetObjectName: { type: z.string(), description: 'The name or catalog identifier of the celestial object to find (e.g., "M13", "Andromeda Galaxy", "Mars").', }, fovDegrees: { type: z.number().positive(), description: "The Field of View (FOV) of the user's equipment in degrees.", }, maxHopMagnitude: { type: z.number().optional().default(8.0), description: 'The maximum (dimmest) stellar magnitude for stars in the hopping path. Default: 8.0.', }, initialSearchRadiusDegrees: { type: z.number().positive().optional().default(20.0), description: 'The angular radius around the target object to search for a suitable bright starting star. Default: 20.0 degrees.', }, startStarMagnitudeThreshold: { type: z.number().optional().default(3.5), description: 'The maximum (dimmest) magnitude for a star to be a good, bright "starting star." Default: 3.5.', }, };
  • Core handler function implementing the star hopping algorithm: selects starting star, iteratively finds hops to stars closer to target within FOV, computes bearings and distances, returns path or error status.
    async execute(params: StarHoppingInput) { const date = new Date(); const observer: Observer = { latitude: OBSERVER_CONFIG.latitude, longitude: OBSERVER_CONFIG.longitude, elevation: OBSERVER_CONFIG.altitude, temperature: OBSERVER_CONFIG.temperature, pressure: OBSERVER_CONFIG.pressure, }; let targetEquatorial: EquatorialCoordinates; try { targetEquatorial = await getEquatorialCoordinates(params.targetObjectName, date); } catch (error: any) { return { targetObjectName: params.targetObjectName, status: 'TargetNotFound', summaryMessage: `Target object "${params.targetObjectName}" not found in catalogs. ${error.message}`, }; } const targetAltAz = convertToAltAz(targetEquatorial, observer, date); if (targetAltAz.altitude <= 0) { return { targetObjectName: params.targetObjectName, targetCoordinates: { ...this.formatCoordsForOutput(targetEquatorial), ...this.formatAltAzForOutput(targetAltAz), }, fieldOfViewDegrees: params.fovDegrees, status: 'TargetNotVisible', summaryMessage: `Target "${params.targetObjectName}" is currently below the horizon.`, }; } const targetData: CelestialObjectData = { ...targetEquatorial, id: params.targetObjectName.toLowerCase(), altAz: targetAltAz, }; // Starting Star Selection let potentialStartStars: CelestialObjectData[] = []; // The above block was removed because it allowed non-star targets (like DSOs) // to be considered as starting stars if they had a magnitude, which is incorrect. // Starting stars must be actual stars from the STAR_CATALOG. for (const [starId, starEq] of STAR_CATALOG.entries()) { if (starEq.magnitude === undefined || starEq.magnitude > params.startStarMagnitudeThreshold!) { continue; } const separation = calculateAngularSeparation(starEq, targetEquatorial); if (separation > params.initialSearchRadiusDegrees!) { continue; } const starAltAz = convertToAltAz(starEq, observer, date); if (starAltAz.altitude <= 0) { continue; } potentialStartStars.push({ ...starEq, id: starId, altAz: starAltAz }); } if (potentialStartStars.length === 0) { return { targetObjectName: params.targetObjectName, targetCoordinates: { ...this.formatCoordsForOutput(targetEquatorial), ...this.formatAltAzForOutput(targetAltAz), }, fieldOfViewDegrees: params.fovDegrees, status: 'NoStartingStarFound', summaryMessage: `No suitable starting star found within ${params.initialSearchRadiusDegrees}° of "${params.targetObjectName}" and brighter than magnitude ${params.startStarMagnitudeThreshold}.`, }; } potentialStartStars.sort((a, b) => (a.magnitude ?? Infinity) - (b.magnitude ?? Infinity)); const startStar = potentialStartStars[0]; const initialSeparationToTarget = calculateAngularSeparation(startStar, targetEquatorial); const baseResponse = { targetObjectName: params.targetObjectName, targetCoordinates: { ...this.formatCoordsForOutput(targetEquatorial), ...this.formatAltAzForOutput(targetAltAz), }, fieldOfViewDegrees: params.fovDegrees, startStar: { name: startStar.name!, magnitude: startStar.magnitude!, ...this.formatCoordsForOutput(startStar), }, }; if (initialSeparationToTarget <= params.fovDegrees) { const bearingToTarget = calculateBearing(startStar, targetEquatorial); return { ...baseResponse, hopSequence: [], finalStep: { fromStar: { name: startStar.name!, magnitude: startStar.magnitude! }, message: `The target ${params.targetObjectName} should be within your FOV, approx ${initialSeparationToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${startStar.name}.`, }, status: 'TargetInStartFOV', summaryMessage: `Target "${params.targetObjectName}" is already within FOV of the starting star "${startStar.name}".`, }; } // Star Hopping Algorithm const hopSequence: any[] = []; let currentHopStar = startStar; let currentDistanceToTarget = initialSeparationToTarget; const visitedStarIds = new Set<string>([startStar.id]); for (let hopNum = 1; hopNum <= 20; hopNum++) { // Max 20 hops to prevent infinite loops let bestNextHop: CelestialObjectData | null = null; let smallestDistToTargetForNextHop = currentDistanceToTarget; for (const [candidateId, candidateEq] of STAR_CATALOG.entries()) { if (visitedStarIds.has(candidateId) || candidateId === targetData.id) { continue; // Skip already visited or the target itself as an intermediate hop } if (candidateEq.magnitude === undefined || candidateEq.magnitude > params.maxHopMagnitude!) { continue; } const hopSeparation = calculateAngularSeparation(currentHopStar, candidateEq); if (hopSeparation > params.fovDegrees) { continue; } const candidateDistToTarget = calculateAngularSeparation(candidateEq, targetEquatorial); if (candidateDistToTarget >= currentDistanceToTarget) { // Must be closer to target continue; } const candidateAltAz = convertToAltAz(candidateEq, observer, date); if (candidateAltAz.altitude <= 0) { continue; } // Prefer candidate that makes most progress towards target if (candidateDistToTarget < smallestDistToTargetForNextHop) { smallestDistToTargetForNextHop = candidateDistToTarget; bestNextHop = { ...candidateEq, id: candidateId }; // altAz is checked for visibility but not stored in the hop object } } if (bestNextHop) { const bearingToNextHop = calculateBearing(currentHopStar, bestNextHop); const hopDistance = calculateAngularSeparation(currentHopStar, bestNextHop); hopSequence.push({ hopNumber: hopNum, fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, toStar: { name: bestNextHop.name!, magnitude: bestNextHop.magnitude!, ...this.formatCoordsForOutput(bestNextHop), }, direction: `towards ${bearingToNextHop.cardinal} (Bearing: ${bearingToNextHop.degrees}°)`, angularDistanceDegrees: parseFloat(hopDistance.toFixed(1)), }); currentHopStar = bestNextHop; currentDistanceToTarget = smallestDistToTargetForNextHop; visitedStarIds.add(currentHopStar.id); if (currentDistanceToTarget <= params.fovDegrees) { const bearingToTarget = calculateBearing(currentHopStar, targetEquatorial); return { ...baseResponse, hopSequence, finalStep: { fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, message: `The target ${params.targetObjectName} should now be within your FOV, approx ${currentDistanceToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${currentHopStar.name}.`, }, status: 'Success', summaryMessage: `Successfully found a path with ${hopSequence.length} hop(s) to "${params.targetObjectName}".`, }; } } else { // No suitable next hop found const bearingToTarget = calculateBearing(currentHopStar, targetEquatorial); return { ...baseResponse, hopSequence, finalStep: { fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, message: `Pathfinding stopped. Target ${params.targetObjectName} is approx ${currentDistanceToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${currentHopStar.name}, but no further hops could be found.`, }, status: 'PathNotFound', summaryMessage: `Could not find a complete hopping path to "${params.targetObjectName}". Path generated with ${hopSequence.length} hop(s).`, }; } } // Max hops reached const bearingToTarget = calculateBearing(currentHopStar, targetEquatorial); return { ...baseResponse, hopSequence, finalStep: { fromStar: { name: currentHopStar.name!, magnitude: currentHopStar.magnitude! }, message: `Pathfinding stopped after maximum hops. Target ${params.targetObjectName} is approx ${currentDistanceToTarget.toFixed(1)}° towards ${bearingToTarget.cardinal} (Bearing: ${bearingToTarget.degrees}°) from ${currentHopStar.name}.`, }, status: 'PathNotFound', summaryMessage: `Path to "${params.targetObjectName}" could not be completed within the maximum hop limit. Path generated with ${hopSequence.length} hop(s).`, }; }
  • Helper methods to format equatorial and alt-az coordinates for output.
    private formatCoordsForOutput(coords: EquatorialCoordinates) { return { rightAscension: `${coords.rightAscension.toFixed(2)}h`, declination: `${coords.declination.toFixed(2)}°`, }; } private formatAltAzForOutput(altAz: HorizontalCoordinates) { return { altitude: `${altAz.altitude.toFixed(1)}°`, azimuth: `${altAz.azimuth.toFixed(1)}°`, }; }

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Rkm1999/CelestialMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server