Skip to main content
Glama
MaxwellCalkin

N2YO Satellite Tracker MCP Server

query_satellites_natural

Answer natural language questions about satellite positions and passes, including location-based queries and category filtering.

Instructions

Answer natural language questions about satellites like 'What satellites will be over France at 6:00 tonight?'

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesNatural language query about satellites (e.g., 'What satellites will be over France at 6:00 tonight?', 'Show me military satellites above Germany now')
categoryFilterNoOptional filter for satellite categoryall

Implementation Reference

  • Main handler function that executes the tool logic: parses natural language query for location and time, fetches satellites above the location via N2YOClient, applies category filter, and returns structured JSON response.
    private async querySatellitesNatural(
      query: string,
      categoryFilter: string = "all"
    ): Promise<CallToolResult> {
      try {
        // Parse the natural language query
        const parsed = LocationTimeParser.extractLocationAndTime(query);
    
        if (!parsed.location) {
          return {
            content: [
              {
                type: "text",
                text: "Could not identify a location in your query. Please specify a location like 'France', 'New York', or 'Germany'.",
              },
            ],
            isError: true,
          };
        }
    
        // Default to current time if no time specified
        const targetTime =
          parsed.time?.timestamp || Math.floor(Date.now() / 1000);
        const timeDescription = parsed.time?.description || "right now";
    
        // Determine which satellites to get based on the query context
        let satellites;
        const categoryId =
          categoryFilter !== "all"
            ? this.n2yoClient.getCategoryId(categoryFilter)
            : 0;
    
        // Check if query is asking about future time (for predictions) or current/past time (for current position)
        const timeDiff = targetTime - Math.floor(Date.now() / 1000);
    
        if (timeDiff > 300) {
          // More than 5 minutes in the future
          // This is a future prediction query - but N2YO "above" endpoint only shows current satellites
          // We'll get current satellites and note the limitation
          satellites = await this.n2yoClient.getSatellitesAbove(
            parsed.location.latitude,
            parsed.location.longitude,
            0, // altitude
            85, // search radius
            categoryId
          );
    
          const filteredSatellites =
            categoryFilter !== "all"
              ? satellites.filter((sat) =>
                  this.matchesCategoryFilter(sat, categoryFilter)
                )
              : satellites;
    
          const response = {
            query: query,
            location: {
              name: parsed.location.name,
              coordinates: {
                latitude: parsed.location.latitude,
                longitude: parsed.location.longitude,
              },
            },
            time: {
              requested: timeDescription,
              note: "Showing satellites currently above the location. N2YO API provides current positions, not future predictions for overhead satellites.",
            },
            categoryFilter: categoryFilter,
            satellites: filteredSatellites.map((sat) => ({
              noradId: sat.satid,
              name: sat.satname,
              position: {
                latitude: sat.satlat,
                longitude: sat.satlng,
                altitude: sat.satalt,
              },
              launchDate: sat.launchDate,
              internationalDesignator: sat.intDesignator,
            })),
            count: filteredSatellites.length,
            summary: `Found ${filteredSatellites.length} ${
              categoryFilter === "all" ? "" : categoryFilter + " "
            }satellites currently above ${parsed.location.name}`,
          };
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(response, null, 2),
              },
            ],
          };
        } else {
          // This is a current or near-current time query
          satellites = await this.n2yoClient.getSatellitesAbove(
            parsed.location.latitude,
            parsed.location.longitude,
            0, // altitude
            85, // search radius
            categoryId
          );
    
          const filteredSatellites =
            categoryFilter !== "all"
              ? satellites.filter((sat) =>
                  this.matchesCategoryFilter(sat, categoryFilter)
                )
              : satellites;
    
          const response = {
            query: query,
            location: {
              name: parsed.location.name,
              coordinates: {
                latitude: parsed.location.latitude,
                longitude: parsed.location.longitude,
              },
            },
            time: {
              description: timeDescription,
              timestamp: targetTime,
            },
            categoryFilter: categoryFilter,
            satellites: filteredSatellites.map((sat) => ({
              noradId: sat.satid,
              name: sat.satname,
              position: {
                latitude: sat.satlat,
                longitude: sat.satlng,
                altitude: sat.satalt,
              },
              launchDate: sat.launchDate,
              internationalDesignator: sat.intDesignator,
            })),
            count: filteredSatellites.length,
            summary: `Found ${filteredSatellites.length} ${
              categoryFilter === "all" ? "" : categoryFilter + " "
            }satellites above ${parsed.location.name} ${timeDescription}`,
          };
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(response, null, 2),
              },
            ],
          };
        }
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error processing natural language query: ${
                error instanceof Error ? error.message : String(error)
              }`,
            },
          ],
          isError: true,
        };
      }
    }
  • src/server.ts:34-63 (registration)
    Tool registration in the getTools() method, defining name, description, and input schema.
    {
      name: "query_satellites_natural",
      description:
        "Answer natural language questions about satellites like 'What satellites will be over France at 6:00 tonight?'",
      inputSchema: {
        type: "object",
        properties: {
          query: {
            type: "string",
            description:
              "Natural language query about satellites (e.g., 'What satellites will be over France at 6:00 tonight?', 'Show me military satellites above Germany now')",
          },
          categoryFilter: {
            type: "string",
            enum: [
              "all",
              "military",
              "weather",
              "gps",
              "amateur",
              "starlink",
              "space-stations",
            ],
            default: "all",
            description: "Optional filter for satellite category",
          },
        },
        required: ["query"],
      },
    },
  • Input schema defining the expected parameters: query (required string) and optional categoryFilter.
    inputSchema: {
      type: "object",
      properties: {
        query: {
          type: "string",
          description:
            "Natural language query about satellites (e.g., 'What satellites will be over France at 6:00 tonight?', 'Show me military satellites above Germany now')",
        },
        categoryFilter: {
          type: "string",
          enum: [
            "all",
            "military",
            "weather",
            "gps",
            "amateur",
            "starlink",
            "space-stations",
          ],
          default: "all",
          description: "Optional filter for satellite category",
        },
      },
      required: ["query"],
    },
  • Dispatch case in callTool method that routes the tool call to the specific handler.
    case "query_satellites_natural":
      return await this.querySatellitesNatural(args.query, args.categoryFilter);
  • Helper function to filter satellites by category based on name keywords.
    private matchesCategoryFilter(
      satellite: any,
      categoryFilter: string
    ): boolean {
      const satName = satellite.satname?.toLowerCase() || "";
    
      switch (categoryFilter) {
        case "military":
          return (
            satName.includes("military") ||
            satName.includes("defense") ||
            satName.includes("nrol") ||
            (satName.includes("usa") && satName.includes("classified"))
          );
    
        case "weather":
          return (
            satName.includes("weather") ||
            satName.includes("noaa") ||
            satName.includes("goes") ||
            satName.includes("meteosat")
          );
    
        case "gps":
          return (
            satName.includes("gps") ||
            satName.includes("navstar") ||
            satName.includes("navigation")
          );
    
        case "amateur":
          return (
            satName.includes("amateur") ||
            satName.includes("amsat") ||
            satName.includes("cubesat")
          );
    
        case "starlink":
          return satName.includes("starlink");
    
        case "space-stations":
          return (
            satName.includes("space station") ||
            satName.includes("iss") ||
            satName.includes("tiangong")
          );
    
        default:
          return true;
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. While it indicates the tool answers questions, it doesn't describe how it processes natural language, what types of answers it provides (e.g., list, summary, detailed report), potential limitations, or error conditions. This leaves significant gaps in understanding the tool's behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, well-structured sentence that efficiently communicates the tool's purpose with relevant examples. Every word serves a purpose, and it's appropriately front-loaded with the core functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no annotations and no output schema, the description provides adequate basic context about what the tool does but lacks details about behavioral characteristics, output format, and limitations. Given the complexity of natural language processing and the absence of structured behavioral hints, more completeness would be beneficial.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents both parameters thoroughly. The description doesn't add any meaningful information about parameters beyond what's in the schema descriptions, maintaining the baseline score for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Answer') and resource ('natural language questions about satellites'), and provides concrete examples that illustrate its function. It distinguishes itself from sibling tools like get_satellites_above or get_satellite_position by emphasizing natural language processing rather than structured queries.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implicitly indicates when to use this tool (for natural language questions about satellites) through its examples, which helps differentiate it from siblings that require structured inputs. However, it doesn't explicitly state when not to use it or name specific alternatives, leaving some ambiguity about tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other 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/MaxwellCalkin/N2YO-MCP'

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