# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── config.ts │ ├── constants.ts │ ├── formatters.ts │ ├── handlers │ │ ├── findParks.ts │ │ ├── getAlerts.ts │ │ ├── getCampgrounds.ts │ │ ├── getEvents.ts │ │ ├── getParkDetails.ts │ │ └── getVisitorCenters.ts │ ├── index.ts │ ├── schemas.ts │ ├── server.ts │ └── utils │ └── npsApiClient.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # dev logs user_stories.md # Build Directory build/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # National Parks MCP Server [](https://smithery.ai/server/@KyrieTangSheng/mcp-server-nationalparks) [](https://mseep.ai/app/8c07fa61-fd4b-4662-8356-908408e45e44) MCP Server for the National Park Service (NPS) API, providing real-time information about U.S. National Parks, including park details, alerts, and activities. ## Tools 1. `findParks` - Search for national parks based on various criteria - Inputs: - `stateCode` (optional string): Filter parks by state code (e.g., "CA" for California). Multiple states can be comma-separated (e.g., "CA,OR,WA") - `q` (optional string): Search term to filter parks by name or description - `limit` (optional number): Maximum number of parks to return (default: 10, max: 50) - `start` (optional number): Start position for results (useful for pagination) - `activities` (optional string): Filter by available activities (e.g., "hiking,camping") - Returns: Matching parks with detailed information 2. `getParkDetails` - Get comprehensive information about a specific national park - Inputs: - `parkCode` (string): The park code of the national park (e.g., "yose" for Yosemite, "grca" for Grand Canyon) - Returns: Detailed park information including descriptions, hours, fees, contacts, and activities 3. `getAlerts` - Get current alerts for national parks including closures, hazards, and important information - Inputs: - `parkCode` (optional string): Filter alerts by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") - `limit` (optional number): Maximum number of alerts to return (default: 10, max: 50) - `start` (optional number): Start position for results (useful for pagination) - `q` (optional string): Search term to filter alerts by title or description - Returns: Current alerts organized by park 4. `getVisitorCenters` - Get information about visitor centers and their operating hours - Inputs: - `parkCode` (optional string): Filter visitor centers by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") - `limit` (optional number): Maximum number of visitor centers to return (default: 10, max: 50) - `start` (optional number): Start position for results (useful for pagination) - `q` (optional string): Search term to filter visitor centers by name or description - Returns: Visitor center information including location, hours, and contact details 5. `getCampgrounds` - Get information about available campgrounds and their amenities - Inputs: - `parkCode` (optional string): Filter campgrounds by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") - `limit` (optional number): Maximum number of campgrounds to return (default: 10, max: 50) - `start` (optional number): Start position for results (useful for pagination) - `q` (optional string): Search term to filter campgrounds by name or description - Returns: Campground information including amenities, fees, and reservation details 6. `getEvents` - Find upcoming events at parks - Inputs: - `parkCode` (optional string): Filter events by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") - `limit` (optional number): Maximum number of events to return (default: 10, max: 50) - `start` (optional number): Start position for results (useful for pagination) - `dateStart` (optional string): Start date for filtering events (format: YYYY-MM-DD) - `dateEnd` (optional string): End date for filtering events (format: YYYY-MM-DD) - `q` (optional string): Search term to filter events by title or description - Returns: Event information including dates, times, and descriptions ## Setup ### Installing via Smithery To install mcp-server-nationalparks for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@KyrieTangSheng/mcp-server-nationalparks): ```bash npx -y @smithery/cli install @KyrieTangSheng/mcp-server-nationalparks --client claude ``` ### NPS API Key 1. Get a free API key from the [National Park Service Developer Portal](https://www.nps.gov/subjects/developer/get-started.htm) 2. Store this key securely as it will be used to authenticate requests ### Usage with Claude Desktop To use this server with Claude Desktop, add the following to your `claude_desktop_config.json`: ```json { "mcpServers": { "nationalparks": { "command": "npx", "args": ["-y", "mcp-server-nationalparks"], "env": { "NPS_API_KEY": "YOUR_NPS_API_KEY" } } } } ``` ## Example Usage ### Finding Parks in a State ``` Tell me about national parks in Colorado. ``` ### Getting Details About a Specific Park ``` What's the entrance fee for Yellowstone National Park? ``` ### Checking for Alerts or Closures ``` Are there any closures or alerts at Yosemite right now? ``` ### Finding Visitor Centers ``` What visitor centers are available at Grand Canyon National Park? ``` ### Looking for Campgrounds ``` Are there any campgrounds with electrical hookups in Zion National Park? ``` ### Finding Upcoming Events ``` What events are happening at Acadia National Park next weekend? ``` ### Planning a Trip Based on Activities ``` Which national parks in Utah have good hiking trails? ``` ## License This MCP server is licensed under the MIT License. See the LICENSE file for details. ## Appendix: Popular National Parks and their codes | Park Name | Park Code | |-----------|-----------| | Yosemite | yose | | Grand Canyon | grca | | Yellowstone | yell | | Zion | zion | | Great Smoky Mountains | grsm | | Acadia | acad | | Olympic | olym | | Rocky Mountain | romo | | Joshua Tree | jotr | | Sequoia & Kings Canyon | seki | For a complete list, visit the [NPS website](https://www.nps.gov/findapark/index.htm). ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile FROM node:lts-alpine WORKDIR /app # Copy package files and install dependencies COPY package*.json ./ RUN npm install --ignore-scripts # Copy all files COPY . . # Build the project RUN npm run build CMD [ "node", "build/index.js" ] ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- ```typescript // List of valid state codes for validation export const STATE_CODES = [ 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 'DC', 'AS', 'GU', 'MP', 'PR', 'VI', 'UM' ]; // Version information export const VERSION = '1.0.0'; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - npsApiKey properties: npsApiKey: type: string description: API key for the National Park Service commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['build/index.js'], env: { NPS_API_KEY: config.npsApiKey } }) exampleConfig: npsApiKey: YOUR_NPS_API_KEY ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import dotenv from 'dotenv'; import { createServer } from './server.js'; // Load environment variables dotenv.config(); // Check for API key if (!process.env.NPS_API_KEY) { console.warn('Warning: NPS_API_KEY is not set in environment variables.'); console.warn('Get your API key at: https://www.nps.gov/subjects/developer/get-started.htm'); } // Start the server async function runServer() { const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("National Parks MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript /** * Configuration for the NPS MCP Server */ import dotenv from 'dotenv'; import path from 'path'; // Load environment variables from .env file dotenv.config({ path: path.resolve(__dirname, '../.env') }); export const config = { // NPS API Configuration npsApiKey: process.env.NPS_API_KEY || '', // Server Configuration serverName: 'mcp-server-nationalparks', serverVersion: '1.0.0', serverDescription: 'MCP server providing real-time data about U.S. national parks', // Logging Configuration logLevel: process.env.LOG_LEVEL || 'info', }; // Validate required configuration if (!config.npsApiKey) { console.warn('Warning: NPS_API_KEY is not set in environment variables. The server will not function correctly without an API key.'); console.warn('Get your API key at: https://www.nps.gov/subjects/developer/get-started.htm'); } export default config; ``` -------------------------------------------------------------------------------- /src/handlers/getParkDetails.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { GetParkDetailsSchema } from '../schemas.js'; import { npsApiClient } from '../utils/npsApiClient.js'; import { formatParkDetails } from '../formatters.js'; export async function getParkDetailsHandler(args: z.infer<typeof GetParkDetailsSchema>) { const response = await npsApiClient.getParkByCode(args.parkCode); // Check if park was found if (!response.data || response.data.length === 0) { return { content: [{ type: "text", text: JSON.stringify({ error: 'Park not found', message: `No park found with park code: ${args.parkCode}` }, null, 2) }] }; } // Format the response for better readability by the AI const parkDetails = formatParkDetails(response.data[0]); return { content: [{ type: "text", text: JSON.stringify(parkDetails, null, 2) }] }; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-server-nationalparks", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "bin": { "mcp-server-nationalparks": "./build/index.js" }, "scripts": { "build": "tsc && chmod 755 build/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "files": [ "build" ], "repository": { "type": "git", "url": "git+https://github.com/KyrieTangSheng/mcp-server-nationalparks.git" }, "keywords": ["mcp", "claude", "national-parks", "api", "anthropic"], "author": "Tang Sheng", "license": "MIT", "bugs": { "url": "https://github.com/KyrieTangSheng/mcp-server-nationalparks/issues" }, "homepage": "https://github.com/KyrieTangSheng/mcp-server-nationalparks#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", "axios": "^1.8.4", "dotenv": "^16.4.7", "zod": "^3.24.2" }, "devDependencies": { "@types/node": "^22.13.10", "ts-node": "^10.9.2", "typescript": "^5.8.2" } } ``` -------------------------------------------------------------------------------- /src/handlers/getAlerts.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { GetAlertsSchema } from '../schemas.js'; import { npsApiClient } from '../utils/npsApiClient.js'; import { formatAlertData } from '../formatters.js'; export async function getAlertsHandler(args: z.infer<typeof GetAlertsSchema>) { // Set default limit if not provided or if it exceeds maximum const limit = args.limit ? Math.min(args.limit, 50) : 10; // Format the request parameters const requestParams = { limit, ...args }; const response = await npsApiClient.getAlerts(requestParams); // Format the response for better readability by the AI const formattedAlerts = formatAlertData(response.data); // Group alerts by park code for better organization const alertsByPark: { [key: string]: any[] } = {}; formattedAlerts.forEach(alert => { if (!alertsByPark[alert.parkCode]) { alertsByPark[alert.parkCode] = []; } alertsByPark[alert.parkCode].push(alert); }); const result = { total: parseInt(response.total), limit: parseInt(response.limit), start: parseInt(response.start), alerts: formattedAlerts, alertsByPark }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ``` -------------------------------------------------------------------------------- /src/handlers/getEvents.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { GetEventsSchema } from '../schemas.js'; import { npsApiClient } from '../utils/npsApiClient.js'; import { formatEventData } from '../formatters.js'; export async function getEventsHandler(args: z.infer<typeof GetEventsSchema>) { // Set default limit if not provided or if it exceeds maximum const limit = args.limit ? Math.min(args.limit, 50) : 10; // Format the request parameters const requestParams = { limit, ...args }; const response = await npsApiClient.getEvents(requestParams); // Format the response for better readability by the AI const formattedEvents = formatEventData(response.data); // Group events by park code for better organization const eventsByPark: { [key: string]: any[] } = {}; formattedEvents.forEach(event => { if (!eventsByPark[event.parkCode]) { eventsByPark[event.parkCode] = []; } eventsByPark[event.parkCode].push(event); }); const result = { total: parseInt(response.total), limit: parseInt(response.limit), start: parseInt(response.start), events: formattedEvents, eventsByPark: eventsByPark }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ``` -------------------------------------------------------------------------------- /src/handlers/getVisitorCenters.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { GetVisitorCentersSchema } from '../schemas.js'; import { npsApiClient } from '../utils/npsApiClient.js'; import { formatVisitorCenterData } from '../formatters.js'; export async function getVisitorCentersHandler(args: z.infer<typeof GetVisitorCentersSchema>) { // Set default limit if not provided or if it exceeds maximum const limit = args.limit ? Math.min(args.limit, 50) : 10; // Format the request parameters const requestParams = { limit, ...args }; const response = await npsApiClient.getVisitorCenters(requestParams); // Format the response for better readability by the AI const formattedCenters = formatVisitorCenterData(response.data); // Group visitor centers by park code for better organization const centersByPark: { [key: string]: any[] } = {}; formattedCenters.forEach(center => { if (!centersByPark[center.parkCode]) { centersByPark[center.parkCode] = []; } centersByPark[center.parkCode].push(center); }); const result = { total: parseInt(response.total), limit: parseInt(response.limit), start: parseInt(response.start), visitorCenters: formattedCenters, visitorCentersByPark: centersByPark }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ``` -------------------------------------------------------------------------------- /src/handlers/getCampgrounds.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { GetCampgroundsSchema } from '../schemas.js'; import { npsApiClient } from '../utils/npsApiClient.js'; import { formatCampgroundData } from '../formatters.js'; export async function getCampgroundsHandler(args: z.infer<typeof GetCampgroundsSchema>) { // Set default limit if not provided or if it exceeds maximum const limit = args.limit ? Math.min(args.limit, 50) : 10; // Format the request parameters const requestParams = { limit, ...args }; const response = await npsApiClient.getCampgrounds(requestParams); // Format the response for better readability by the AI const formattedCampgrounds = formatCampgroundData(response.data); // Group campgrounds by park code for better organization const campgroundsByPark: { [key: string]: any[] } = {}; formattedCampgrounds.forEach(campground => { if (!campgroundsByPark[campground.parkCode]) { campgroundsByPark[campground.parkCode] = []; } campgroundsByPark[campground.parkCode].push(campground); }); const result = { total: parseInt(response.total), limit: parseInt(response.limit), start: parseInt(response.start), campgrounds: formattedCampgrounds, campgroundsByPark: campgroundsByPark }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ``` -------------------------------------------------------------------------------- /src/handlers/findParks.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { FindParksSchema } from '../schemas.js'; import { npsApiClient } from '../utils/npsApiClient.js'; import { formatParkData } from '../formatters.js'; import { STATE_CODES } from '../constants.js'; export async function findParksHandler(args: z.infer<typeof FindParksSchema>) { // Validate state codes if provided if (args.stateCode) { const providedStates = args.stateCode.split(',').map(s => s.trim().toUpperCase()); const invalidStates = providedStates.filter(state => !STATE_CODES.includes(state)); if (invalidStates.length > 0) { return { content: [{ type: "text", text: JSON.stringify({ error: `Invalid state code(s): ${invalidStates.join(', ')}`, validStateCodes: STATE_CODES }) }] }; } } // Set default limit if not provided or if it exceeds maximum const limit = args.limit ? Math.min(args.limit, 50) : 10; // Format the request parameters const requestParams = { limit, ...args }; const response = await npsApiClient.getParks(requestParams); // Format the response for better readability by the AI const formattedParks = formatParkData(response.data); const result = { total: parseInt(response.total), limit: parseInt(response.limit), start: parseInt(response.start), parks: formattedParks }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ``` -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; // Find Parks Schema export const FindParksSchema = z.object({ stateCode: z.string().optional().describe('Filter parks by state code (e.g., "CA" for California, "NY" for New York). Multiple states can be comma-separated (e.g., "CA,OR,WA")'), q: z.string().optional().describe('Search term to filter parks by name or description'), limit: z.number().optional().describe('Maximum number of parks to return (default: 10, max: 50)'), start: z.number().optional().describe('Start position for results (useful for pagination)'), activities: z.string().optional().describe('Filter by available activities (e.g., "hiking,camping")') }); // Get Park Details Schema export const GetParkDetailsSchema = z.object({ parkCode: z.string().describe('The park code of the national park (e.g., "yose" for Yosemite, "grca" for Grand Canyon)') }); // Get Alerts Schema export const GetAlertsSchema = z.object({ parkCode: z.string().optional().describe('Filter alerts by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca").'), limit: z.number().optional().describe('Maximum number of alerts to return (default: 10, max: 50)'), start: z.number().optional().describe('Start position for results (useful for pagination)'), q: z.string().optional().describe('Search term to filter alerts by title or description') }); // Get Visitor Centers Schema export const GetVisitorCentersSchema = z.object({ parkCode: z.string().optional().describe('Filter visitor centers by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca").'), limit: z.number().optional().describe('Maximum number of visitor centers to return (default: 10, max: 50)'), start: z.number().optional().describe('Start position for results (useful for pagination)'), q: z.string().optional().describe('Search term to filter visitor centers by name or description') }); // Get Campgrounds Schema export const GetCampgroundsSchema = z.object({ parkCode: z.string().optional().describe('Filter campgrounds by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca").'), limit: z.number().optional().describe('Maximum number of campgrounds to return (default: 10, max: 50)'), start: z.number().optional().describe('Start position for results (useful for pagination)'), q: z.string().optional().describe('Search term to filter campgrounds by name or description') }); // Get Events Schema export const GetEventsSchema = z.object({ parkCode: z.string().optional().describe('Filter events by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca").'), limit: z.number().optional().describe('Maximum number of events to return (default: 10, max: 50)'), start: z.number().optional().describe('Start position for results (useful for pagination)'), dateStart: z.string().optional().describe('Start date for filtering events (format: YYYY-MM-DD)'), dateEnd: z.string().optional().describe('End date for filtering events (format: YYYY-MM-DD)'), q: z.string().optional().describe('Search term to filter events by title or description') }); ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { VERSION } from './constants.js'; import { FindParksSchema, GetParkDetailsSchema, GetAlertsSchema, GetVisitorCentersSchema, GetCampgroundsSchema, GetEventsSchema } from './schemas.js'; import { findParksHandler } from './handlers/findParks.js'; import { getParkDetailsHandler } from './handlers/getParkDetails.js'; import { getAlertsHandler } from './handlers/getAlerts.js'; import { getVisitorCentersHandler } from './handlers/getVisitorCenters.js'; import { getCampgroundsHandler } from './handlers/getCampgrounds.js'; import { getEventsHandler } from './handlers/getEvents.js'; // Create and configure the server export function createServer() { const server = new Server( { name: "nationalparks-mcp-server", version: VERSION, }, { capabilities: { tools: {}, }, } ); // Register tool definitions server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "findParks", description: "Search for national parks based on state, name, activities, or other criteria", inputSchema: zodToJsonSchema(FindParksSchema), }, { name: "getParkDetails", description: "Get detailed information about a specific national park", inputSchema: zodToJsonSchema(GetParkDetailsSchema), }, { name: "getAlerts", description: "Get current alerts for national parks including closures, hazards, and important information", inputSchema: zodToJsonSchema(GetAlertsSchema), }, { name: "getVisitorCenters", description: "Get information about visitor centers and their operating hours", inputSchema: zodToJsonSchema(GetVisitorCentersSchema), }, { name: "getCampgrounds", description: "Get information about available campgrounds and their amenities", inputSchema: zodToJsonSchema(GetCampgroundsSchema), }, { name: "getEvents", description: "Find upcoming events at parks", inputSchema: zodToJsonSchema(GetEventsSchema), }, ], }; }); // Handle tool executions server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (!request.params.arguments) { throw new Error("Arguments are required"); } switch (request.params.name) { case "findParks": { const args = FindParksSchema.parse(request.params.arguments); return await findParksHandler(args); } case "getParkDetails": { const args = GetParkDetailsSchema.parse(request.params.arguments); return await getParkDetailsHandler(args); } case "getAlerts": { const args = GetAlertsSchema.parse(request.params.arguments); return await getAlertsHandler(args); } case "getVisitorCenters": { const args = GetVisitorCentersSchema.parse(request.params.arguments); return await getVisitorCentersHandler(args); } case "getCampgrounds": { const args = GetCampgroundsSchema.parse(request.params.arguments); return await getCampgroundsHandler(args); } case "getEvents": { const args = GetEventsSchema.parse(request.params.arguments); return await getEventsHandler(args); } default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { if (error instanceof z.ZodError) { return { content: [{ type: "text", text: JSON.stringify({ error: 'Validation error', details: error.errors }, null, 2) }] }; } console.error('Error executing tool:', error); return { content: [{ type: "text", text: JSON.stringify({ error: 'Server error', message: error instanceof Error ? error.message : 'Unknown error' }, null, 2) }] }; } }); return server; } ``` -------------------------------------------------------------------------------- /src/utils/npsApiClient.ts: -------------------------------------------------------------------------------- ```typescript /** * NPS API Client * * A client for interacting with the National Park Service API. * https://www.nps.gov/subjects/developer/api-documentation.htm */ import axios, { AxiosInstance } from 'axios'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); // Define types for API responses export interface NPSResponse<T> { total: string; limit: string; start: string; data: T[]; } export interface ParkData { id: string; url: string; fullName: string; parkCode: string; description: string; latitude: string; longitude: string; latLong: string; activities: Array<{ id: string; name: string }>; topics: Array<{ id: string; name: string }>; states: string; contacts: { phoneNumbers: Array<{ phoneNumber: string; description: string; extension: string; type: string }>; emailAddresses: Array<{ description: string; emailAddress: string }>; }; entranceFees: Array<{ cost: string; description: string; title: string }>; entrancePasses: Array<{ cost: string; description: string; title: string }>; fees: any[]; directionsInfo: string; directionsUrl: string; operatingHours: Array<{ exceptions: any[]; description: string; standardHours: { sunday: string; monday: string; tuesday: string; wednesday: string; thursday: string; friday: string; saturday: string; }; name: string; }>; addresses: Array<{ postalCode: string; city: string; stateCode: string; line1: string; line2: string; line3: string; type: string; }>; images: Array<{ credit: string; title: string; altText: string; caption: string; url: string; }>; weatherInfo: string; name: string; designation: string; } export interface AlertData { id: string; url: string; title: string; parkCode: string; description: string; category: string; lastIndexedDate: string; } // Define parameter types for the API methods export interface ParkQueryParams { parkCode?: string; stateCode?: string; limit?: number; start?: number; q?: string; fields?: string; } export interface AlertQueryParams { parkCode?: string; limit?: number; start?: number; q?: string; } export interface VisitorCenterData { id: string; url: string; name: string; parkCode: string; description: string; latitude: string; longitude: string; latLong: string; directionsInfo: string; directionsUrl: string; addresses: Array<{ postalCode: string; city: string; stateCode: string; line1: string; line2: string; line3: string; type: string; }>; operatingHours: Array<{ exceptions: any[]; description: string; standardHours: { sunday: string; monday: string; tuesday: string; wednesday: string; thursday: string; friday: string; saturday: string; }; name: string; }>; contacts: { phoneNumbers: Array<{ phoneNumber: string; description: string; extension: string; type: string }>; emailAddresses: Array<{ description: string; emailAddress: string }>; }; } export interface CampgroundData { id: string; url: string; name: string; parkCode: string; description: string; latitude: string; longitude: string; latLong: string; audioDescription: string; isPassportStampLocation: boolean; passportStampLocationDescription: string; passportStampImages: any[]; geometryPoiId: string; reservationInfo: string; reservationUrl: string; regulationsurl: string; regulationsOverview: string; amenities: { trashRecyclingCollection: boolean; toilets: string[]; internetConnectivity: boolean; showers: string[]; cellPhoneReception: boolean; laundry: boolean; amphitheater: boolean; dumpStation: boolean; campStore: boolean; staffOrVolunteerHostOnsite: boolean; potableWater: string[]; iceAvailableForSale: boolean; firewoodForSale: boolean; foodStorageLockers: boolean; }; contacts: { phoneNumbers: Array<{ phoneNumber: string; description: string; extension: string; type: string }>; emailAddresses: Array<{ description: string; emailAddress: string }>; }; fees: Array<{ cost: string; description: string; title: string; }>; directionsOverview: string; directionsUrl: string; operatingHours: Array<{ exceptions: any[]; description: string; standardHours: { sunday: string; monday: string; tuesday: string; wednesday: string; thursday: string; friday: string; saturday: string; }; name: string; }>; addresses: Array<{ postalCode: string; city: string; stateCode: string; line1: string; line2: string; line3: string; type: string; }>; weatherOverview: string; numberOfSitesReservable: string; numberOfSitesFirstComeFirstServe: string; campsites: { totalSites: string; group: string; horse: string; tentOnly: string; electricalHookups: string; rvOnly: string; walkBoatTo: string; other: string; }; accessibility: { wheelchairAccess: string; internetInfo: string; cellPhoneInfo: string; fireStovePolicy: string; rvAllowed: boolean; rvInfo: string; rvMaxLength: string; additionalInfo: string; trailerMaxLength: string; adaInfo: string; trailerAllowed: boolean; accessRoads: string[]; classifications: string[]; }; } export interface EventData { id: string; url: string; title: string; parkFullName: string; description: string; latitude: string; longitude: string; category: string; subcategory: string; location: string; tags: string[]; recurrenceDateStart: string; recurrenceDateEnd: string; times: Array<{ timeStart: string; timeEnd: string; sunriseTimeStart: boolean; sunsetTimeEnd: boolean; }>; dates: string[]; dateStart: string; dateEnd: string; regresurl: string; contactEmailAddress: string; contactTelephoneNumber: string; feeInfo: string; isRecurring: boolean; isAllDay: boolean; siteCode: string; parkCode: string; organizationName: string; types: string[]; createDate: string; lastUpdated: string; infoURL: string; portalName: string; } export interface VisitorCenterQueryParams { parkCode?: string; limit?: number; start?: number; q?: string; } export interface CampgroundQueryParams { parkCode?: string; limit?: number; start?: number; q?: string; } export interface EventQueryParams { parkCode?: string; limit?: number; start?: number; q?: string; dateStart?: string; dateEnd?: string; } /** * NPS API Client class */ class NPSApiClient { private api: AxiosInstance; private baseUrl: string = 'https://developer.nps.gov/api/v1'; private apiKey: string; constructor() { this.apiKey = process.env.NPS_API_KEY || ''; if (!this.apiKey) { console.warn('Warning: NPS_API_KEY is not set in environment variables.'); console.warn('Get your API key at: https://www.nps.gov/subjects/developer/get-started.htm'); } // Create axios instance for NPS API this.api = axios.create({ baseURL: this.baseUrl, headers: { 'X-Api-Key': this.apiKey, }, }); // Add response interceptor for error handling this.api.interceptors.response.use( (response) => response, (error) => { if (error.response) { // Check for rate limiting if (error.response.status === 429) { console.error('Rate limit exceeded for NPS API. Please try again later.'); } // Log the error details console.error('NPS API Error:', { status: error.response.status, statusText: error.response.statusText, data: error.response.data, }); } else if (error.request) { console.error('No response received from NPS API:', error.request); } else { console.error('Error setting up NPS API request:', error.message); } return Promise.reject(error); } ); } /** * Fetch parks data from the NPS API * @param params Query parameters * @returns Promise with parks data */ async getParks(params: ParkQueryParams = {}): Promise<NPSResponse<ParkData>> { try { const response = await this.api.get('/parks', { params }); return response.data; } catch (error) { console.error('Error fetching parks data:', error); throw error; } } /** * Fetch a specific park by its parkCode * @param parkCode The park code (e.g., 'yose' for Yosemite) * @returns Promise with the park data */ async getParkByCode(parkCode: string): Promise<NPSResponse<ParkData>> { try { const response = await this.api.get('/parks', { params: { parkCode, limit: 1 } }); return response.data; } catch (error) { console.error(`Error fetching park with code ${parkCode}:`, error); throw error; } } /** * Fetch alerts from the NPS API * @param params Query parameters * @returns Promise with alerts data */ async getAlerts(params: AlertQueryParams = {}): Promise<NPSResponse<AlertData>> { try { const response = await this.api.get('/alerts', { params }); return response.data; } catch (error) { console.error('Error fetching alerts data:', error); throw error; } } /** * Fetch alerts for a specific park * @param parkCode The park code (e.g., 'yose' for Yosemite) * @returns Promise with the park's alerts */ async getAlertsByParkCode(parkCode: string): Promise<NPSResponse<AlertData>> { try { const response = await this.api.get('/alerts', { params: { parkCode } }); return response.data; } catch (error) { console.error(`Error fetching alerts for park ${parkCode}:`, error); throw error; } } /** * Fetch visitor centers from the NPS API * @param params Query parameters * @returns Promise with visitor centers data */ async getVisitorCenters(params: VisitorCenterQueryParams = {}): Promise<NPSResponse<VisitorCenterData>> { try { const response = await this.api.get('/visitorcenters', { params }); return response.data; } catch (error) { console.error('Error fetching visitor centers data:', error); throw error; } } /** * Fetch campgrounds from the NPS API * @param params Query parameters * @returns Promise with campgrounds data */ async getCampgrounds(params: CampgroundQueryParams = {}): Promise<NPSResponse<CampgroundData>> { try { const response = await this.api.get('/campgrounds', { params }); return response.data; } catch (error) { console.error('Error fetching campgrounds data:', error); throw error; } } /** * Fetch events from the NPS API * @param params Query parameters * @returns Promise with events data */ async getEvents(params: EventQueryParams = {}): Promise<NPSResponse<EventData>> { try { const response = await this.api.get('/events', { params }); return response.data; } catch (error) { console.error('Error fetching events data:', error); throw error; } } } // Export a singleton instance export const npsApiClient = new NPSApiClient(); ``` -------------------------------------------------------------------------------- /src/formatters.ts: -------------------------------------------------------------------------------- ```typescript import { ParkData, AlertData, VisitorCenterData, CampgroundData, EventData } from './utils/npsApiClient.js'; /** * Format the park data into a more readable format for LLMs */ export function formatParkData(parkData: ParkData[]) { return parkData.map(park => ({ name: park.fullName, code: park.parkCode, description: park.description, states: park.states.split(',').map(code => code.trim()), url: park.url, designation: park.designation, activities: park.activities.map(activity => activity.name), weatherInfo: park.weatherInfo, location: { latitude: park.latitude, longitude: park.longitude }, entranceFees: park.entranceFees.map(fee => ({ cost: fee.cost, description: fee.description, title: fee.title })), operatingHours: park.operatingHours.map(hours => ({ name: hours.name, description: hours.description, standardHours: hours.standardHours })), contacts: { phoneNumbers: park.contacts.phoneNumbers.map(phone => ({ type: phone.type, number: phone.phoneNumber, description: phone.description })), emailAddresses: park.contacts.emailAddresses.map(email => ({ address: email.emailAddress, description: email.description })) }, images: park.images.map(image => ({ url: image.url, title: image.title, altText: image.altText, caption: image.caption, credit: image.credit })) })); } /** * Format park details for a single park */ export function formatParkDetails(park: ParkData) { // Determine the best address to use as the primary address const physicalAddress = park.addresses.find(addr => addr.type === 'Physical') || park.addresses[0]; // Format operating hours in a more readable way const formattedHours = park.operatingHours.map(hours => { const { standardHours } = hours; const formattedStandardHours = Object.entries(standardHours) .map(([day, hours]) => { // Convert day to proper case (e.g., 'monday' to 'Monday') const properDay = day.charAt(0).toUpperCase() + day.slice(1); return `${properDay}: ${hours || 'Closed'}`; }); return { name: hours.name, description: hours.description, standardHours: formattedStandardHours }; }); return { name: park.fullName, code: park.parkCode, url: park.url, description: park.description, designation: park.designation, states: park.states.split(',').map(code => code.trim()), weatherInfo: park.weatherInfo, directionsInfo: park.directionsInfo, directionsUrl: park.directionsUrl, location: { latitude: park.latitude, longitude: park.longitude, address: physicalAddress ? { line1: physicalAddress.line1, line2: physicalAddress.line2, city: physicalAddress.city, stateCode: physicalAddress.stateCode, postalCode: physicalAddress.postalCode } : undefined }, contacts: { phoneNumbers: park.contacts.phoneNumbers.map(phone => ({ type: phone.type, number: phone.phoneNumber, extension: phone.extension, description: phone.description })), emailAddresses: park.contacts.emailAddresses.map(email => ({ address: email.emailAddress, description: email.description })) }, entranceFees: park.entranceFees.map(fee => ({ title: fee.title, cost: `$${fee.cost}`, description: fee.description })), entrancePasses: park.entrancePasses.map(pass => ({ title: pass.title, cost: `$${pass.cost}`, description: pass.description })), operatingHours: formattedHours, topics: park.topics.map(topic => topic.name), activities: park.activities.map(activity => activity.name), images: park.images.map(image => ({ url: image.url, title: image.title, altText: image.altText, caption: image.caption, credit: image.credit })) }; } /** * Format the alert data into a more readable format for LLMs */ export function formatAlertData(alertData: AlertData[]) { return alertData.map(alert => { // Get the date part from the lastIndexedDate (which is in ISO format) const lastUpdated = alert.lastIndexedDate ? new Date(alert.lastIndexedDate).toLocaleDateString() : 'Unknown'; // Categorize the alert type let alertType = alert.category; if (alertType === 'Information') { alertType = 'Information (non-emergency)'; } else if (alertType === 'Caution') { alertType = 'Caution (potential hazard)'; } else if (alertType === 'Danger') { alertType = 'Danger (significant hazard)'; } else if (alertType === 'Park Closure') { alertType = 'Park Closure (area inaccessible)'; } return { title: alert.title, description: alert.description, parkCode: alert.parkCode, type: alertType, url: alert.url, lastUpdated }; }); } /** * Format visitor center data for better readability */ export function formatVisitorCenterData(visitorCenterData: VisitorCenterData[]) { return visitorCenterData.map(center => { // Find physical address if available const physicalAddress = center.addresses.find(addr => addr.type === 'Physical') || center.addresses[0]; // Format operating hours const formattedHours = center.operatingHours.map(hours => { const { standardHours } = hours; const formattedStandardHours = Object.entries(standardHours) .map(([day, hours]) => { // Convert day to proper case (e.g., 'monday' to 'Monday') const properDay = day.charAt(0).toUpperCase() + day.slice(1); return `${properDay}: ${hours || 'Closed'}`; }); return { name: hours.name, description: hours.description, standardHours: formattedStandardHours }; }); return { name: center.name, parkCode: center.parkCode, description: center.description, url: center.url, directionsInfo: center.directionsInfo, directionsUrl: center.directionsUrl, location: { latitude: center.latitude, longitude: center.longitude, address: physicalAddress ? { line1: physicalAddress.line1, line2: physicalAddress.line2, city: physicalAddress.city, stateCode: physicalAddress.stateCode, postalCode: physicalAddress.postalCode } : undefined }, operatingHours: formattedHours, contacts: { phoneNumbers: center.contacts.phoneNumbers.map(phone => ({ type: phone.type, number: phone.phoneNumber, extension: phone.extension, description: phone.description })), emailAddresses: center.contacts.emailAddresses.map(email => ({ address: email.emailAddress, description: email.description })) } }; }); } /** * Format campground data for better readability */ export function formatCampgroundData(campgroundData: CampgroundData[]) { return campgroundData.map(campground => { // Find physical address if available const physicalAddress = campground.addresses.find(addr => addr.type === 'Physical') || campground.addresses[0]; // Format operating hours const formattedHours = campground.operatingHours.map(hours => { const { standardHours } = hours; const formattedStandardHours = Object.entries(standardHours) .map(([day, hours]) => { const properDay = day.charAt(0).toUpperCase() + day.slice(1); return `${properDay}: ${hours || 'Closed'}`; }); return { name: hours.name, description: hours.description, standardHours: formattedStandardHours }; }); // Format amenities for better readability const amenities = []; if (campground.amenities) { if (campground.amenities.trashRecyclingCollection) amenities.push('Trash/Recycling Collection'); if (campground.amenities.toilets && campground.amenities.toilets.length > 0) amenities.push(`Toilets (${campground.amenities.toilets.join(', ')})`); if (campground.amenities.internetConnectivity) amenities.push('Internet Connectivity'); if (campground.amenities.showers && campground.amenities.showers.length > 0) amenities.push(`Showers (${campground.amenities.showers.join(', ')})`); if (campground.amenities.cellPhoneReception) amenities.push('Cell Phone Reception'); if (campground.amenities.laundry) amenities.push('Laundry'); if (campground.amenities.amphitheater) amenities.push('Amphitheater'); if (campground.amenities.dumpStation) amenities.push('Dump Station'); if (campground.amenities.campStore) amenities.push('Camp Store'); if (campground.amenities.staffOrVolunteerHostOnsite) amenities.push('Staff/Volunteer Host Onsite'); if (campground.amenities.potableWater && campground.amenities.potableWater.length > 0) amenities.push(`Potable Water (${campground.amenities.potableWater.join(', ')})`); if (campground.amenities.iceAvailableForSale) amenities.push('Ice Available For Sale'); if (campground.amenities.firewoodForSale) amenities.push('Firewood For Sale'); if (campground.amenities.foodStorageLockers) amenities.push('Food Storage Lockers'); } return { name: campground.name, parkCode: campground.parkCode, description: campground.description, url: campground.url, reservationInfo: campground.reservationInfo, reservationUrl: campground.reservationUrl, regulations: campground.regulationsOverview, regulationsUrl: campground.regulationsurl, weatherOverview: campground.weatherOverview, location: { latitude: campground.latitude, longitude: campground.longitude, address: physicalAddress ? { line1: physicalAddress.line1, line2: physicalAddress.line2, city: physicalAddress.city, stateCode: physicalAddress.stateCode, postalCode: physicalAddress.postalCode } : undefined }, operatingHours: formattedHours, fees: campground.fees.map(fee => ({ title: fee.title, cost: `$${fee.cost}`, description: fee.description })), totalSites: campground.campsites?.totalSites || '0', sitesReservable: campground.numberOfSitesReservable || '0', sitesFirstComeFirstServe: campground.numberOfSitesFirstComeFirstServe || '0', campsiteTypes: { group: campground.campsites?.group || '0', horse: campground.campsites?.horse || '0', tentOnly: campground.campsites?.tentOnly || '0', electricalHookups: campground.campsites?.electricalHookups || '0', rvOnly: campground.campsites?.rvOnly || '0', walkBoatTo: campground.campsites?.walkBoatTo || '0', other: campground.campsites?.other || '0' }, amenities: amenities, accessibility: { wheelchairAccess: campground.accessibility?.wheelchairAccess, rvAllowed: campground.accessibility?.rvAllowed, rvMaxLength: campground.accessibility?.rvMaxLength, trailerAllowed: campground.accessibility?.trailerAllowed, trailerMaxLength: campground.accessibility?.trailerMaxLength, accessRoads: campground.accessibility?.accessRoads, adaInfo: campground.accessibility?.adaInfo }, contacts: { phoneNumbers: campground.contacts.phoneNumbers.map(phone => ({ type: phone.type, number: phone.phoneNumber, extension: phone.extension, description: phone.description })), emailAddresses: campground.contacts.emailAddresses.map(email => ({ address: email.emailAddress, description: email.description })) } }; }); } /** * Format event data for better readability */ export function formatEventData(eventData: EventData[]) { return eventData.map(event => { // Format dates and times const formattedDates = event.dates ? event.dates.join(', ') : ''; // Format times const formattedTimes = event.times.map(time => { let timeString = ''; if (time.timeStart) { timeString += time.sunriseTimeStart ? 'Sunrise' : time.timeStart; } if (time.timeEnd) { timeString += ' to '; timeString += time.sunsetTimeEnd ? 'Sunset' : time.timeEnd; } return timeString || 'All day'; }).join(', '); return { title: event.title, parkCode: event.parkCode, parkName: event.parkFullName, description: event.description, category: event.category, subcategory: event.subcategory, tags: event.tags, location: event.location, coordinates: { latitude: event.latitude, longitude: event.longitude }, dateTime: { dates: formattedDates, times: formattedTimes, dateStart: event.dateStart, dateEnd: event.dateEnd, isAllDay: event.isAllDay, isRecurring: event.isRecurring, recurrenceDateStart: event.recurrenceDateStart, recurrenceDateEnd: event.recurrenceDateEnd }, feeInfo: event.feeInfo, contactInfo: { email: event.contactEmailAddress, phone: event.contactTelephoneNumber }, infoUrl: event.infoURL || event.url, lastUpdated: event.lastUpdated }; }); } ```