Skip to main content
Glama

APRS.fi MCP Server

by dhhuston
index.ts14 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; // Import APRS service interfaces and types interface APRSPosition { name: string; callsign: string; lat: number; lng: number; altitude?: number; timestamp: number; comment?: string; speed?: number; course?: number; symbol?: string; path?: string; } interface APRSResponse { command: string; result: string; what: string; found: number; entries: APRSPosition[]; } class APRSError extends Error { constructor(message: string, public status?: number) { super(message); this.name = 'APRSError'; } } class APRSMCPService { private readonly baseUrl = 'https://api.aprs.fi/api/get'; private rateLimitDelay = 1000; private apiKey: string | null = null; setApiKey(apiKey: string): void { this.apiKey = apiKey; } getApiKey(): string | null { return this.apiKey; } clearApiKey(): void { this.apiKey = null; } async getPosition(callsign: string, apiKey?: string): Promise<APRSPosition[]> { const keyToUse = apiKey || this.apiKey; if (!keyToUse) { throw new APRSError('APRS API key not provided. Use /set-api-key command or provide apiKey parameter.'); } if (!callsign?.trim()) { throw new APRSError('Callsign is required'); } const params = new URLSearchParams({ name: callsign.trim().toUpperCase(), what: 'loc', apikey: keyToUse, format: 'json' }); try { const response = await fetch(`${this.baseUrl}?${params}`); if (!response.ok) { throw new APRSError( `APRS API request failed: ${response.status} ${response.statusText}`, response.status ); } const data: APRSResponse = await response.json(); if (data.result !== 'ok') { throw new APRSError(`APRS API error: ${data.result}`); } if (data.found === 0) { return []; } return data.entries.map(entry => ({ ...entry, timestamp: entry.timestamp * 1000, })); } catch (error) { if (error instanceof APRSError) { throw error; } if (error instanceof TypeError && error.message.includes('fetch')) { throw new APRSError('Network error: Unable to connect to APRS.fi API. Check your internet connection.'); } throw new APRSError(`Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getPositionHistory( callsign: string, apiKey?: string, lastHours: number = 24 ): Promise<APRSPosition[]> { const keyToUse = apiKey || this.apiKey; if (!keyToUse) { throw new APRSError('APRS API key not provided. Use /set-api-key command or provide apiKey parameter.'); } if (!callsign?.trim()) { throw new APRSError('Callsign is required'); } const params = new URLSearchParams({ name: callsign.trim().toUpperCase(), what: 'loc', apikey: keyToUse, format: 'json', last: lastHours.toString() }); try { const response = await fetch(`${this.baseUrl}?${params}`); if (!response.ok) { throw new APRSError( `APRS API request failed: ${response.status} ${response.statusText}`, response.status ); } const data: APRSResponse = await response.json(); if (data.result !== 'ok') { throw new APRSError(`APRS API error: ${data.result}`); } return data.entries.map(entry => ({ ...entry, timestamp: entry.timestamp * 1000, })).sort((a, b) => a.timestamp - b.timestamp); } catch (error) { if (error instanceof APRSError) { throw error; } if (error instanceof TypeError && error.message.includes('fetch')) { throw new APRSError('Network error: Unable to connect to APRS.fi API. Check your internet connection.'); } throw new APRSError(`Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getMultiplePositions(callsigns: string[], apiKey?: string): Promise<APRSPosition[]> { const keyToUse = apiKey || this.apiKey; if (!keyToUse) { throw new APRSError('APRS API key not provided. Use /set-api-key command or provide apiKey parameter.'); } if (!callsigns?.length) { return []; } const cleanCallsigns = callsigns .map(c => c.trim().toUpperCase()) .filter(c => c.length > 0); if (cleanCallsigns.length === 0) { return []; } const params = new URLSearchParams({ name: cleanCallsigns.join(','), what: 'loc', apikey: keyToUse, format: 'json' }); try { const response = await fetch(`${this.baseUrl}?${params}`); if (!response.ok) { throw new APRSError( `APRS API request failed: ${response.status} ${response.statusText}`, response.status ); } const data: APRSResponse = await response.json(); if (data.result !== 'ok') { throw new APRSError(`APRS API error: ${data.result}`); } return data.entries.map(entry => ({ ...entry, timestamp: entry.timestamp * 1000, })); } catch (error) { if (error instanceof APRSError) { throw error; } if (error instanceof TypeError && error.message.includes('fetch')) { throw new APRSError('Network error: Unable to connect to APRS.fi API. Check your internet connection.'); } throw new APRSError(`Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async validateApiKey(apiKey: string): Promise<boolean> { if (!apiKey) { return false; } try { const params = new URLSearchParams({ name: 'TEST', what: 'loc', apikey: apiKey, format: 'json' }); const response = await fetch(`${this.baseUrl}?${params}`); if (!response.ok) { return false; } const data: APRSResponse = await response.json(); return data.result === 'ok'; } catch { return false; } } } class APRSMCPServer { private server: Server; private aprsService: APRSMCPService; constructor() { this.server = new Server( { name: 'aprs-fi-server', version: '1.0.0', }, { capabilities: { tools: {}, prompts: {}, }, } ); this.aprsService = new APRSMCPService(); this.setupToolHandlers(); this.setupPromptHandlers(); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_aprs_position', description: 'Get current position data for a specific callsign from APRS.fi', inputSchema: { type: 'object', properties: { callsign: { type: 'string', description: 'The callsign to look up (e.g., "W1AW")', }, apiKey: { type: 'string', description: 'APRS.fi API key (optional if set via /set-api-key)', }, }, required: ['callsign'], }, }, { name: 'get_aprs_history', description: 'Get position history for a callsign with time range', inputSchema: { type: 'object', properties: { callsign: { type: 'string', description: 'The callsign to look up', }, apiKey: { type: 'string', description: 'APRS.fi API key (optional if set via /set-api-key)', }, lastHours: { type: 'number', description: 'Number of hours to look back (default: 24)', default: 24, }, }, required: ['callsign'], }, }, { name: 'track_multiple_callsigns', description: 'Track multiple callsigns at once', inputSchema: { type: 'object', properties: { callsigns: { type: 'array', items: { type: 'string', }, description: 'Array of callsigns to track', }, apiKey: { type: 'string', description: 'APRS.fi API key (optional if set via /set-api-key)', }, }, required: ['callsigns'], }, }, { name: 'validate_aprs_key', description: 'Test if an APRS.fi API key is valid', inputSchema: { type: 'object', properties: { apiKey: { type: 'string', description: 'APRS.fi API key to validate', }, }, required: ['apiKey'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { throw new McpError( ErrorCode.InvalidParams, 'Missing arguments' ); } try { switch (name) { case 'get_aprs_position': const positions = await this.aprsService.getPosition( args.callsign as string, args.apiKey as string ); return { content: [ { type: 'text', text: JSON.stringify(positions, null, 2), }, ], }; case 'get_aprs_history': const history = await this.aprsService.getPositionHistory( args.callsign as string, args.apiKey as string, args.lastHours as number ); return { content: [ { type: 'text', text: JSON.stringify(history, null, 2), }, ], }; case 'track_multiple_callsigns': const multiplePositions = await this.aprsService.getMultiplePositions( args.callsigns as string[], args.apiKey as string ); return { content: [ { type: 'text', text: JSON.stringify(multiplePositions, null, 2), }, ], }; case 'validate_aprs_key': const isValid = await this.aprsService.validateApiKey( args.apiKey as string ); return { content: [ { type: 'text', text: JSON.stringify({ valid: isValid }, null, 2), }, ], }; default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof APRSError) { // Use InvalidParams for missing API key to provide better error handling if (error.message.includes('API key not provided')) { throw new McpError( ErrorCode.InvalidParams, error.message ); } throw new McpError( ErrorCode.InternalError, `APRS Error: ${error.message}` ); } throw new McpError( ErrorCode.InternalError, `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); } private setupPromptHandlers() { this.server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: 'set-api-key', description: 'Set the APRS.fi API key for this session', arguments: [ { name: 'api_key', description: 'Your APRS.fi API key', required: true, }, ], }, ], }; }); this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'set-api-key') { const apiKey = args?.api_key as string; if (!apiKey) { throw new McpError( ErrorCode.InvalidParams, 'API key is required' ); } this.aprsService.setApiKey(apiKey); return { description: 'Set APRS.fi API key', messages: [ { role: 'user', content: { type: 'text', text: `APRS.fi API key has been set successfully. You can now use APRS tools without specifying the apiKey parameter.`, }, }, ], }; } throw new McpError( ErrorCode.InvalidParams, `Unknown prompt: ${name}` ); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('APRS.fi MCP server running on stdio'); } } const server = new APRSMCPServer(); server.run().catch(console.error);

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/dhhuston/APRSFI-MCP-SERVER'

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