# 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: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | # dev logs 139 | user_stories.md 140 | 141 | # Build Directory 142 | build/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # National Parks MCP Server 2 | [](https://smithery.ai/server/@KyrieTangSheng/mcp-server-nationalparks) 3 | [](https://mseep.ai/app/8c07fa61-fd4b-4662-8356-908408e45e44) 4 | 5 | MCP Server for the National Park Service (NPS) API, providing real-time information about U.S. National Parks, including park details, alerts, and activities. 6 | 7 | ## Tools 8 | 9 | 1. `findParks` 10 | - Search for national parks based on various criteria 11 | - Inputs: 12 | - `stateCode` (optional string): Filter parks by state code (e.g., "CA" for California). Multiple states can be comma-separated (e.g., "CA,OR,WA") 13 | - `q` (optional string): Search term to filter parks by name or description 14 | - `limit` (optional number): Maximum number of parks to return (default: 10, max: 50) 15 | - `start` (optional number): Start position for results (useful for pagination) 16 | - `activities` (optional string): Filter by available activities (e.g., "hiking,camping") 17 | - Returns: Matching parks with detailed information 18 | 19 | 2. `getParkDetails` 20 | - Get comprehensive information about a specific national park 21 | - Inputs: 22 | - `parkCode` (string): The park code of the national park (e.g., "yose" for Yosemite, "grca" for Grand Canyon) 23 | - Returns: Detailed park information including descriptions, hours, fees, contacts, and activities 24 | 25 | 3. `getAlerts` 26 | - Get current alerts for national parks including closures, hazards, and important information 27 | - Inputs: 28 | - `parkCode` (optional string): Filter alerts by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") 29 | - `limit` (optional number): Maximum number of alerts to return (default: 10, max: 50) 30 | - `start` (optional number): Start position for results (useful for pagination) 31 | - `q` (optional string): Search term to filter alerts by title or description 32 | - Returns: Current alerts organized by park 33 | 34 | 4. `getVisitorCenters` 35 | - Get information about visitor centers and their operating hours 36 | - Inputs: 37 | - `parkCode` (optional string): Filter visitor centers by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") 38 | - `limit` (optional number): Maximum number of visitor centers to return (default: 10, max: 50) 39 | - `start` (optional number): Start position for results (useful for pagination) 40 | - `q` (optional string): Search term to filter visitor centers by name or description 41 | - Returns: Visitor center information including location, hours, and contact details 42 | 43 | 5. `getCampgrounds` 44 | - Get information about available campgrounds and their amenities 45 | - Inputs: 46 | - `parkCode` (optional string): Filter campgrounds by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") 47 | - `limit` (optional number): Maximum number of campgrounds to return (default: 10, max: 50) 48 | - `start` (optional number): Start position for results (useful for pagination) 49 | - `q` (optional string): Search term to filter campgrounds by name or description 50 | - Returns: Campground information including amenities, fees, and reservation details 51 | 52 | 6. `getEvents` 53 | - Find upcoming events at parks 54 | - Inputs: 55 | - `parkCode` (optional string): Filter events by park code (e.g., "yose" for Yosemite). Multiple parks can be comma-separated (e.g., "yose,grca") 56 | - `limit` (optional number): Maximum number of events to return (default: 10, max: 50) 57 | - `start` (optional number): Start position for results (useful for pagination) 58 | - `dateStart` (optional string): Start date for filtering events (format: YYYY-MM-DD) 59 | - `dateEnd` (optional string): End date for filtering events (format: YYYY-MM-DD) 60 | - `q` (optional string): Search term to filter events by title or description 61 | - Returns: Event information including dates, times, and descriptions 62 | 63 | ## Setup 64 | 65 | ### Installing via Smithery 66 | 67 | To install mcp-server-nationalparks for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@KyrieTangSheng/mcp-server-nationalparks): 68 | 69 | ```bash 70 | npx -y @smithery/cli install @KyrieTangSheng/mcp-server-nationalparks --client claude 71 | ``` 72 | 73 | ### NPS API Key 74 | 1. Get a free API key from the [National Park Service Developer Portal](https://www.nps.gov/subjects/developer/get-started.htm) 75 | 2. Store this key securely as it will be used to authenticate requests 76 | 77 | ### Usage with Claude Desktop 78 | 79 | To use this server with Claude Desktop, add the following to your `claude_desktop_config.json`: 80 | 81 | ```json 82 | { 83 | "mcpServers": { 84 | "nationalparks": { 85 | "command": "npx", 86 | "args": ["-y", "mcp-server-nationalparks"], 87 | "env": { 88 | "NPS_API_KEY": "YOUR_NPS_API_KEY" 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | ## Example Usage 95 | 96 | ### Finding Parks in a State 97 | ``` 98 | Tell me about national parks in Colorado. 99 | ``` 100 | 101 | ### Getting Details About a Specific Park 102 | ``` 103 | What's the entrance fee for Yellowstone National Park? 104 | ``` 105 | 106 | ### Checking for Alerts or Closures 107 | ``` 108 | Are there any closures or alerts at Yosemite right now? 109 | ``` 110 | 111 | ### Finding Visitor Centers 112 | ``` 113 | What visitor centers are available at Grand Canyon National Park? 114 | ``` 115 | 116 | ### Looking for Campgrounds 117 | ``` 118 | Are there any campgrounds with electrical hookups in Zion National Park? 119 | ``` 120 | 121 | ### Finding Upcoming Events 122 | ``` 123 | What events are happening at Acadia National Park next weekend? 124 | ``` 125 | 126 | ### Planning a Trip Based on Activities 127 | ``` 128 | Which national parks in Utah have good hiking trails? 129 | ``` 130 | 131 | ## License 132 | 133 | This MCP server is licensed under the MIT License. See the LICENSE file for details. 134 | 135 | 136 | ## Appendix: Popular National Parks and their codes 137 | 138 | | Park Name | Park Code | 139 | |-----------|-----------| 140 | | Yosemite | yose | 141 | | Grand Canyon | grca | 142 | | Yellowstone | yell | 143 | | Zion | zion | 144 | | Great Smoky Mountains | grsm | 145 | | Acadia | acad | 146 | | Olympic | olym | 147 | | Rocky Mountain | romo | 148 | | Joshua Tree | jotr | 149 | | Sequoia & Kings Canyon | seki | 150 | 151 | For a complete list, visit the [NPS website](https://www.nps.gov/findapark/index.htm). 152 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | WORKDIR /app 5 | 6 | # Copy package files and install dependencies 7 | COPY package*.json ./ 8 | 9 | RUN npm install --ignore-scripts 10 | 11 | # Copy all files 12 | COPY . . 13 | 14 | # Build the project 15 | RUN npm run build 16 | 17 | CMD [ "node", "build/index.js" ] 18 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } ``` -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- ```typescript 1 | // List of valid state codes for validation 2 | export const STATE_CODES = [ 3 | 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 4 | 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 5 | 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 6 | 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 7 | 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 8 | 'DC', 'AS', 'GU', 'MP', 'PR', 'VI', 'UM' 9 | ]; 10 | 11 | // Version information 12 | export const VERSION = '1.0.0'; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - npsApiKey 10 | properties: 11 | npsApiKey: 12 | type: string 13 | description: API key for the National Park Service 14 | commandFunction: 15 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 16 | |- 17 | (config) => ({ command: 'node', args: ['build/index.js'], env: { NPS_API_KEY: config.npsApiKey } }) 18 | exampleConfig: 19 | npsApiKey: YOUR_NPS_API_KEY 20 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import dotenv from 'dotenv'; 4 | import { createServer } from './server.js'; 5 | 6 | // Load environment variables 7 | dotenv.config(); 8 | 9 | // Check for API key 10 | if (!process.env.NPS_API_KEY) { 11 | console.warn('Warning: NPS_API_KEY is not set in environment variables.'); 12 | console.warn('Get your API key at: https://www.nps.gov/subjects/developer/get-started.htm'); 13 | } 14 | 15 | // Start the server 16 | async function runServer() { 17 | const server = createServer(); 18 | const transport = new StdioServerTransport(); 19 | await server.connect(transport); 20 | console.error("National Parks MCP Server running on stdio"); 21 | } 22 | 23 | runServer().catch((error) => { 24 | console.error("Fatal error in main():", error); 25 | process.exit(1); 26 | }); ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Configuration for the NPS MCP Server 3 | */ 4 | 5 | import dotenv from 'dotenv'; 6 | import path from 'path'; 7 | 8 | // Load environment variables from .env file 9 | dotenv.config({ path: path.resolve(__dirname, '../.env') }); 10 | 11 | export const config = { 12 | // NPS API Configuration 13 | npsApiKey: process.env.NPS_API_KEY || '', 14 | 15 | // Server Configuration 16 | serverName: 'mcp-server-nationalparks', 17 | serverVersion: '1.0.0', 18 | serverDescription: 'MCP server providing real-time data about U.S. national parks', 19 | 20 | // Logging Configuration 21 | logLevel: process.env.LOG_LEVEL || 'info', 22 | }; 23 | 24 | // Validate required configuration 25 | if (!config.npsApiKey) { 26 | console.warn('Warning: NPS_API_KEY is not set in environment variables. The server will not function correctly without an API key.'); 27 | console.warn('Get your API key at: https://www.nps.gov/subjects/developer/get-started.htm'); 28 | } 29 | 30 | export default config; ``` -------------------------------------------------------------------------------- /src/handlers/getParkDetails.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { GetParkDetailsSchema } from '../schemas.js'; 3 | import { npsApiClient } from '../utils/npsApiClient.js'; 4 | import { formatParkDetails } from '../formatters.js'; 5 | 6 | export async function getParkDetailsHandler(args: z.infer<typeof GetParkDetailsSchema>) { 7 | const response = await npsApiClient.getParkByCode(args.parkCode); 8 | 9 | // Check if park was found 10 | if (!response.data || response.data.length === 0) { 11 | return { 12 | content: [{ 13 | type: "text", 14 | text: JSON.stringify({ 15 | error: 'Park not found', 16 | message: `No park found with park code: ${args.parkCode}` 17 | }, null, 2) 18 | }] 19 | }; 20 | } 21 | 22 | // Format the response for better readability by the AI 23 | const parkDetails = formatParkDetails(response.data[0]); 24 | 25 | return { 26 | content: [{ 27 | type: "text", 28 | text: JSON.stringify(parkDetails, null, 2) 29 | }] 30 | }; 31 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-server-nationalparks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-server-nationalparks": "./build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && chmod 755 build/index.js", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "files": [ 15 | "build" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/KyrieTangSheng/mcp-server-nationalparks.git" 20 | }, 21 | "keywords": ["mcp", "claude", "national-parks", "api", "anthropic"], 22 | "author": "Tang Sheng", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/KyrieTangSheng/mcp-server-nationalparks/issues" 26 | }, 27 | "homepage": "https://github.com/KyrieTangSheng/mcp-server-nationalparks#readme", 28 | "dependencies": { 29 | "@modelcontextprotocol/sdk": "^1.7.0", 30 | "axios": "^1.8.4", 31 | "dotenv": "^16.4.7", 32 | "zod": "^3.24.2" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^22.13.10", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.8.2" 38 | } 39 | } 40 | ``` -------------------------------------------------------------------------------- /src/handlers/getAlerts.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { GetAlertsSchema } from '../schemas.js'; 3 | import { npsApiClient } from '../utils/npsApiClient.js'; 4 | import { formatAlertData } from '../formatters.js'; 5 | 6 | export async function getAlertsHandler(args: z.infer<typeof GetAlertsSchema>) { 7 | // Set default limit if not provided or if it exceeds maximum 8 | const limit = args.limit ? Math.min(args.limit, 50) : 10; 9 | 10 | // Format the request parameters 11 | const requestParams = { 12 | limit, 13 | ...args 14 | }; 15 | 16 | const response = await npsApiClient.getAlerts(requestParams); 17 | 18 | // Format the response for better readability by the AI 19 | const formattedAlerts = formatAlertData(response.data); 20 | 21 | // Group alerts by park code for better organization 22 | const alertsByPark: { [key: string]: any[] } = {}; 23 | formattedAlerts.forEach(alert => { 24 | if (!alertsByPark[alert.parkCode]) { 25 | alertsByPark[alert.parkCode] = []; 26 | } 27 | alertsByPark[alert.parkCode].push(alert); 28 | }); 29 | 30 | const result = { 31 | total: parseInt(response.total), 32 | limit: parseInt(response.limit), 33 | start: parseInt(response.start), 34 | alerts: formattedAlerts, 35 | alertsByPark 36 | }; 37 | 38 | return { 39 | content: [{ 40 | type: "text", 41 | text: JSON.stringify(result, null, 2) 42 | }] 43 | }; 44 | } ``` -------------------------------------------------------------------------------- /src/handlers/getEvents.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { GetEventsSchema } from '../schemas.js'; 3 | import { npsApiClient } from '../utils/npsApiClient.js'; 4 | import { formatEventData } from '../formatters.js'; 5 | 6 | export async function getEventsHandler(args: z.infer<typeof GetEventsSchema>) { 7 | // Set default limit if not provided or if it exceeds maximum 8 | const limit = args.limit ? Math.min(args.limit, 50) : 10; 9 | 10 | // Format the request parameters 11 | const requestParams = { 12 | limit, 13 | ...args 14 | }; 15 | 16 | const response = await npsApiClient.getEvents(requestParams); 17 | 18 | // Format the response for better readability by the AI 19 | const formattedEvents = formatEventData(response.data); 20 | 21 | // Group events by park code for better organization 22 | const eventsByPark: { [key: string]: any[] } = {}; 23 | formattedEvents.forEach(event => { 24 | if (!eventsByPark[event.parkCode]) { 25 | eventsByPark[event.parkCode] = []; 26 | } 27 | eventsByPark[event.parkCode].push(event); 28 | }); 29 | 30 | const result = { 31 | total: parseInt(response.total), 32 | limit: parseInt(response.limit), 33 | start: parseInt(response.start), 34 | events: formattedEvents, 35 | eventsByPark: eventsByPark 36 | }; 37 | 38 | return { 39 | content: [{ 40 | type: "text", 41 | text: JSON.stringify(result, null, 2) 42 | }] 43 | }; 44 | } ``` -------------------------------------------------------------------------------- /src/handlers/getVisitorCenters.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { GetVisitorCentersSchema } from '../schemas.js'; 3 | import { npsApiClient } from '../utils/npsApiClient.js'; 4 | import { formatVisitorCenterData } from '../formatters.js'; 5 | 6 | export async function getVisitorCentersHandler(args: z.infer<typeof GetVisitorCentersSchema>) { 7 | // Set default limit if not provided or if it exceeds maximum 8 | const limit = args.limit ? Math.min(args.limit, 50) : 10; 9 | 10 | // Format the request parameters 11 | const requestParams = { 12 | limit, 13 | ...args 14 | }; 15 | 16 | const response = await npsApiClient.getVisitorCenters(requestParams); 17 | 18 | // Format the response for better readability by the AI 19 | const formattedCenters = formatVisitorCenterData(response.data); 20 | 21 | // Group visitor centers by park code for better organization 22 | const centersByPark: { [key: string]: any[] } = {}; 23 | formattedCenters.forEach(center => { 24 | if (!centersByPark[center.parkCode]) { 25 | centersByPark[center.parkCode] = []; 26 | } 27 | centersByPark[center.parkCode].push(center); 28 | }); 29 | 30 | const result = { 31 | total: parseInt(response.total), 32 | limit: parseInt(response.limit), 33 | start: parseInt(response.start), 34 | visitorCenters: formattedCenters, 35 | visitorCentersByPark: centersByPark 36 | }; 37 | 38 | return { 39 | content: [{ 40 | type: "text", 41 | text: JSON.stringify(result, null, 2) 42 | }] 43 | }; 44 | } ``` -------------------------------------------------------------------------------- /src/handlers/getCampgrounds.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { GetCampgroundsSchema } from '../schemas.js'; 3 | import { npsApiClient } from '../utils/npsApiClient.js'; 4 | import { formatCampgroundData } from '../formatters.js'; 5 | 6 | export async function getCampgroundsHandler(args: z.infer<typeof GetCampgroundsSchema>) { 7 | // Set default limit if not provided or if it exceeds maximum 8 | const limit = args.limit ? Math.min(args.limit, 50) : 10; 9 | 10 | // Format the request parameters 11 | const requestParams = { 12 | limit, 13 | ...args 14 | }; 15 | 16 | const response = await npsApiClient.getCampgrounds(requestParams); 17 | 18 | // Format the response for better readability by the AI 19 | const formattedCampgrounds = formatCampgroundData(response.data); 20 | 21 | // Group campgrounds by park code for better organization 22 | const campgroundsByPark: { [key: string]: any[] } = {}; 23 | formattedCampgrounds.forEach(campground => { 24 | if (!campgroundsByPark[campground.parkCode]) { 25 | campgroundsByPark[campground.parkCode] = []; 26 | } 27 | campgroundsByPark[campground.parkCode].push(campground); 28 | }); 29 | 30 | const result = { 31 | total: parseInt(response.total), 32 | limit: parseInt(response.limit), 33 | start: parseInt(response.start), 34 | campgrounds: formattedCampgrounds, 35 | campgroundsByPark: campgroundsByPark 36 | }; 37 | 38 | return { 39 | content: [{ 40 | type: "text", 41 | text: JSON.stringify(result, null, 2) 42 | }] 43 | }; 44 | } ``` -------------------------------------------------------------------------------- /src/handlers/findParks.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { FindParksSchema } from '../schemas.js'; 3 | import { npsApiClient } from '../utils/npsApiClient.js'; 4 | import { formatParkData } from '../formatters.js'; 5 | import { STATE_CODES } from '../constants.js'; 6 | 7 | export async function findParksHandler(args: z.infer<typeof FindParksSchema>) { 8 | // Validate state codes if provided 9 | if (args.stateCode) { 10 | const providedStates = args.stateCode.split(',').map(s => s.trim().toUpperCase()); 11 | const invalidStates = providedStates.filter(state => !STATE_CODES.includes(state)); 12 | 13 | if (invalidStates.length > 0) { 14 | return { 15 | content: [{ 16 | type: "text", 17 | text: JSON.stringify({ 18 | error: `Invalid state code(s): ${invalidStates.join(', ')}`, 19 | validStateCodes: STATE_CODES 20 | }) 21 | }] 22 | }; 23 | } 24 | } 25 | 26 | // Set default limit if not provided or if it exceeds maximum 27 | const limit = args.limit ? Math.min(args.limit, 50) : 10; 28 | 29 | // Format the request parameters 30 | const requestParams = { 31 | limit, 32 | ...args 33 | }; 34 | 35 | const response = await npsApiClient.getParks(requestParams); 36 | 37 | // Format the response for better readability by the AI 38 | const formattedParks = formatParkData(response.data); 39 | 40 | const result = { 41 | total: parseInt(response.total), 42 | limit: parseInt(response.limit), 43 | start: parseInt(response.start), 44 | parks: formattedParks 45 | }; 46 | 47 | return { 48 | content: [{ 49 | type: "text", 50 | text: JSON.stringify(result, null, 2) 51 | }] 52 | }; 53 | } ``` -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | // Find Parks Schema 4 | export const FindParksSchema = z.object({ 5 | 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")'), 6 | q: z.string().optional().describe('Search term to filter parks by name or description'), 7 | limit: z.number().optional().describe('Maximum number of parks to return (default: 10, max: 50)'), 8 | start: z.number().optional().describe('Start position for results (useful for pagination)'), 9 | activities: z.string().optional().describe('Filter by available activities (e.g., "hiking,camping")') 10 | }); 11 | 12 | // Get Park Details Schema 13 | export const GetParkDetailsSchema = z.object({ 14 | parkCode: z.string().describe('The park code of the national park (e.g., "yose" for Yosemite, "grca" for Grand Canyon)') 15 | }); 16 | 17 | // Get Alerts Schema 18 | export const GetAlertsSchema = z.object({ 19 | 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").'), 20 | limit: z.number().optional().describe('Maximum number of alerts to return (default: 10, max: 50)'), 21 | start: z.number().optional().describe('Start position for results (useful for pagination)'), 22 | q: z.string().optional().describe('Search term to filter alerts by title or description') 23 | }); 24 | 25 | // Get Visitor Centers Schema 26 | export const GetVisitorCentersSchema = z.object({ 27 | 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").'), 28 | limit: z.number().optional().describe('Maximum number of visitor centers to return (default: 10, max: 50)'), 29 | start: z.number().optional().describe('Start position for results (useful for pagination)'), 30 | q: z.string().optional().describe('Search term to filter visitor centers by name or description') 31 | }); 32 | 33 | // Get Campgrounds Schema 34 | export const GetCampgroundsSchema = z.object({ 35 | 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").'), 36 | limit: z.number().optional().describe('Maximum number of campgrounds to return (default: 10, max: 50)'), 37 | start: z.number().optional().describe('Start position for results (useful for pagination)'), 38 | q: z.string().optional().describe('Search term to filter campgrounds by name or description') 39 | }); 40 | 41 | // Get Events Schema 42 | export const GetEventsSchema = z.object({ 43 | 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").'), 44 | limit: z.number().optional().describe('Maximum number of events to return (default: 10, max: 50)'), 45 | start: z.number().optional().describe('Start position for results (useful for pagination)'), 46 | dateStart: z.string().optional().describe('Start date for filtering events (format: YYYY-MM-DD)'), 47 | dateEnd: z.string().optional().describe('End date for filtering events (format: YYYY-MM-DD)'), 48 | q: z.string().optional().describe('Search term to filter events by title or description') 49 | }); ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { 3 | CallToolRequestSchema, 4 | ListToolsRequestSchema, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | import { z } from 'zod'; 7 | import { zodToJsonSchema } from 'zod-to-json-schema'; 8 | 9 | import { VERSION } from './constants.js'; 10 | import { 11 | FindParksSchema, 12 | GetParkDetailsSchema, 13 | GetAlertsSchema, 14 | GetVisitorCentersSchema, 15 | GetCampgroundsSchema, 16 | GetEventsSchema 17 | } from './schemas.js'; 18 | import { findParksHandler } from './handlers/findParks.js'; 19 | import { getParkDetailsHandler } from './handlers/getParkDetails.js'; 20 | import { getAlertsHandler } from './handlers/getAlerts.js'; 21 | import { getVisitorCentersHandler } from './handlers/getVisitorCenters.js'; 22 | import { getCampgroundsHandler } from './handlers/getCampgrounds.js'; 23 | import { getEventsHandler } from './handlers/getEvents.js'; 24 | 25 | // Create and configure the server 26 | export function createServer() { 27 | const server = new Server( 28 | { 29 | name: "nationalparks-mcp-server", 30 | version: VERSION, 31 | }, 32 | { 33 | capabilities: { 34 | tools: {}, 35 | }, 36 | } 37 | ); 38 | 39 | // Register tool definitions 40 | server.setRequestHandler(ListToolsRequestSchema, async () => { 41 | return { 42 | tools: [ 43 | { 44 | name: "findParks", 45 | description: "Search for national parks based on state, name, activities, or other criteria", 46 | inputSchema: zodToJsonSchema(FindParksSchema), 47 | }, 48 | { 49 | name: "getParkDetails", 50 | description: "Get detailed information about a specific national park", 51 | inputSchema: zodToJsonSchema(GetParkDetailsSchema), 52 | }, 53 | { 54 | name: "getAlerts", 55 | description: "Get current alerts for national parks including closures, hazards, and important information", 56 | inputSchema: zodToJsonSchema(GetAlertsSchema), 57 | }, 58 | { 59 | name: "getVisitorCenters", 60 | description: "Get information about visitor centers and their operating hours", 61 | inputSchema: zodToJsonSchema(GetVisitorCentersSchema), 62 | }, 63 | { 64 | name: "getCampgrounds", 65 | description: "Get information about available campgrounds and their amenities", 66 | inputSchema: zodToJsonSchema(GetCampgroundsSchema), 67 | }, 68 | { 69 | name: "getEvents", 70 | description: "Find upcoming events at parks", 71 | inputSchema: zodToJsonSchema(GetEventsSchema), 72 | }, 73 | ], 74 | }; 75 | }); 76 | 77 | // Handle tool executions 78 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 79 | try { 80 | if (!request.params.arguments) { 81 | throw new Error("Arguments are required"); 82 | } 83 | 84 | switch (request.params.name) { 85 | case "findParks": { 86 | const args = FindParksSchema.parse(request.params.arguments); 87 | return await findParksHandler(args); 88 | } 89 | 90 | case "getParkDetails": { 91 | const args = GetParkDetailsSchema.parse(request.params.arguments); 92 | return await getParkDetailsHandler(args); 93 | } 94 | 95 | case "getAlerts": { 96 | const args = GetAlertsSchema.parse(request.params.arguments); 97 | return await getAlertsHandler(args); 98 | } 99 | 100 | case "getVisitorCenters": { 101 | const args = GetVisitorCentersSchema.parse(request.params.arguments); 102 | return await getVisitorCentersHandler(args); 103 | } 104 | 105 | case "getCampgrounds": { 106 | const args = GetCampgroundsSchema.parse(request.params.arguments); 107 | return await getCampgroundsHandler(args); 108 | } 109 | 110 | case "getEvents": { 111 | const args = GetEventsSchema.parse(request.params.arguments); 112 | return await getEventsHandler(args); 113 | } 114 | 115 | default: 116 | throw new Error(`Unknown tool: ${request.params.name}`); 117 | } 118 | } catch (error) { 119 | if (error instanceof z.ZodError) { 120 | return { 121 | content: [{ 122 | type: "text", 123 | text: JSON.stringify({ 124 | error: 'Validation error', 125 | details: error.errors 126 | }, null, 2) 127 | }] 128 | }; 129 | } 130 | 131 | console.error('Error executing tool:', error); 132 | return { 133 | content: [{ 134 | type: "text", 135 | text: JSON.stringify({ 136 | error: 'Server error', 137 | message: error instanceof Error ? error.message : 'Unknown error' 138 | }, null, 2) 139 | }] 140 | }; 141 | } 142 | }); 143 | 144 | return server; 145 | } ``` -------------------------------------------------------------------------------- /src/utils/npsApiClient.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * NPS API Client 3 | * 4 | * A client for interacting with the National Park Service API. 5 | * https://www.nps.gov/subjects/developer/api-documentation.htm 6 | */ 7 | 8 | import axios, { AxiosInstance } from 'axios'; 9 | import dotenv from 'dotenv'; 10 | 11 | // Load environment variables 12 | dotenv.config(); 13 | 14 | // Define types for API responses 15 | export interface NPSResponse<T> { 16 | total: string; 17 | limit: string; 18 | start: string; 19 | data: T[]; 20 | } 21 | 22 | export interface ParkData { 23 | id: string; 24 | url: string; 25 | fullName: string; 26 | parkCode: string; 27 | description: string; 28 | latitude: string; 29 | longitude: string; 30 | latLong: string; 31 | activities: Array<{ id: string; name: string }>; 32 | topics: Array<{ id: string; name: string }>; 33 | states: string; 34 | contacts: { 35 | phoneNumbers: Array<{ phoneNumber: string; description: string; extension: string; type: string }>; 36 | emailAddresses: Array<{ description: string; emailAddress: string }>; 37 | }; 38 | entranceFees: Array<{ cost: string; description: string; title: string }>; 39 | entrancePasses: Array<{ cost: string; description: string; title: string }>; 40 | fees: any[]; 41 | directionsInfo: string; 42 | directionsUrl: string; 43 | operatingHours: Array<{ 44 | exceptions: any[]; 45 | description: string; 46 | standardHours: { 47 | sunday: string; 48 | monday: string; 49 | tuesday: string; 50 | wednesday: string; 51 | thursday: string; 52 | friday: string; 53 | saturday: string; 54 | }; 55 | name: string; 56 | }>; 57 | addresses: Array<{ 58 | postalCode: string; 59 | city: string; 60 | stateCode: string; 61 | line1: string; 62 | line2: string; 63 | line3: string; 64 | type: string; 65 | }>; 66 | images: Array<{ 67 | credit: string; 68 | title: string; 69 | altText: string; 70 | caption: string; 71 | url: string; 72 | }>; 73 | weatherInfo: string; 74 | name: string; 75 | designation: string; 76 | } 77 | 78 | export interface AlertData { 79 | id: string; 80 | url: string; 81 | title: string; 82 | parkCode: string; 83 | description: string; 84 | category: string; 85 | lastIndexedDate: string; 86 | } 87 | 88 | // Define parameter types for the API methods 89 | export interface ParkQueryParams { 90 | parkCode?: string; 91 | stateCode?: string; 92 | limit?: number; 93 | start?: number; 94 | q?: string; 95 | fields?: string; 96 | } 97 | 98 | export interface AlertQueryParams { 99 | parkCode?: string; 100 | limit?: number; 101 | start?: number; 102 | q?: string; 103 | } 104 | 105 | export interface VisitorCenterData { 106 | id: string; 107 | url: string; 108 | name: string; 109 | parkCode: string; 110 | description: string; 111 | latitude: string; 112 | longitude: string; 113 | latLong: string; 114 | directionsInfo: string; 115 | directionsUrl: string; 116 | addresses: Array<{ 117 | postalCode: string; 118 | city: string; 119 | stateCode: string; 120 | line1: string; 121 | line2: string; 122 | line3: string; 123 | type: string; 124 | }>; 125 | operatingHours: Array<{ 126 | exceptions: any[]; 127 | description: string; 128 | standardHours: { 129 | sunday: string; 130 | monday: string; 131 | tuesday: string; 132 | wednesday: string; 133 | thursday: string; 134 | friday: string; 135 | saturday: string; 136 | }; 137 | name: string; 138 | }>; 139 | contacts: { 140 | phoneNumbers: Array<{ phoneNumber: string; description: string; extension: string; type: string }>; 141 | emailAddresses: Array<{ description: string; emailAddress: string }>; 142 | }; 143 | } 144 | 145 | export interface CampgroundData { 146 | id: string; 147 | url: string; 148 | name: string; 149 | parkCode: string; 150 | description: string; 151 | latitude: string; 152 | longitude: string; 153 | latLong: string; 154 | audioDescription: string; 155 | isPassportStampLocation: boolean; 156 | passportStampLocationDescription: string; 157 | passportStampImages: any[]; 158 | geometryPoiId: string; 159 | reservationInfo: string; 160 | reservationUrl: string; 161 | regulationsurl: string; 162 | regulationsOverview: string; 163 | amenities: { 164 | trashRecyclingCollection: boolean; 165 | toilets: string[]; 166 | internetConnectivity: boolean; 167 | showers: string[]; 168 | cellPhoneReception: boolean; 169 | laundry: boolean; 170 | amphitheater: boolean; 171 | dumpStation: boolean; 172 | campStore: boolean; 173 | staffOrVolunteerHostOnsite: boolean; 174 | potableWater: string[]; 175 | iceAvailableForSale: boolean; 176 | firewoodForSale: boolean; 177 | foodStorageLockers: boolean; 178 | }; 179 | contacts: { 180 | phoneNumbers: Array<{ phoneNumber: string; description: string; extension: string; type: string }>; 181 | emailAddresses: Array<{ description: string; emailAddress: string }>; 182 | }; 183 | fees: Array<{ 184 | cost: string; 185 | description: string; 186 | title: string; 187 | }>; 188 | directionsOverview: string; 189 | directionsUrl: string; 190 | operatingHours: Array<{ 191 | exceptions: any[]; 192 | description: string; 193 | standardHours: { 194 | sunday: string; 195 | monday: string; 196 | tuesday: string; 197 | wednesday: string; 198 | thursday: string; 199 | friday: string; 200 | saturday: string; 201 | }; 202 | name: string; 203 | }>; 204 | addresses: Array<{ 205 | postalCode: string; 206 | city: string; 207 | stateCode: string; 208 | line1: string; 209 | line2: string; 210 | line3: string; 211 | type: string; 212 | }>; 213 | weatherOverview: string; 214 | numberOfSitesReservable: string; 215 | numberOfSitesFirstComeFirstServe: string; 216 | campsites: { 217 | totalSites: string; 218 | group: string; 219 | horse: string; 220 | tentOnly: string; 221 | electricalHookups: string; 222 | rvOnly: string; 223 | walkBoatTo: string; 224 | other: string; 225 | }; 226 | accessibility: { 227 | wheelchairAccess: string; 228 | internetInfo: string; 229 | cellPhoneInfo: string; 230 | fireStovePolicy: string; 231 | rvAllowed: boolean; 232 | rvInfo: string; 233 | rvMaxLength: string; 234 | additionalInfo: string; 235 | trailerMaxLength: string; 236 | adaInfo: string; 237 | trailerAllowed: boolean; 238 | accessRoads: string[]; 239 | classifications: string[]; 240 | }; 241 | } 242 | 243 | export interface EventData { 244 | id: string; 245 | url: string; 246 | title: string; 247 | parkFullName: string; 248 | description: string; 249 | latitude: string; 250 | longitude: string; 251 | category: string; 252 | subcategory: string; 253 | location: string; 254 | tags: string[]; 255 | recurrenceDateStart: string; 256 | recurrenceDateEnd: string; 257 | times: Array<{ 258 | timeStart: string; 259 | timeEnd: string; 260 | sunriseTimeStart: boolean; 261 | sunsetTimeEnd: boolean; 262 | }>; 263 | dates: string[]; 264 | dateStart: string; 265 | dateEnd: string; 266 | regresurl: string; 267 | contactEmailAddress: string; 268 | contactTelephoneNumber: string; 269 | feeInfo: string; 270 | isRecurring: boolean; 271 | isAllDay: boolean; 272 | siteCode: string; 273 | parkCode: string; 274 | organizationName: string; 275 | types: string[]; 276 | createDate: string; 277 | lastUpdated: string; 278 | infoURL: string; 279 | portalName: string; 280 | } 281 | 282 | export interface VisitorCenterQueryParams { 283 | parkCode?: string; 284 | limit?: number; 285 | start?: number; 286 | q?: string; 287 | } 288 | 289 | export interface CampgroundQueryParams { 290 | parkCode?: string; 291 | limit?: number; 292 | start?: number; 293 | q?: string; 294 | } 295 | 296 | export interface EventQueryParams { 297 | parkCode?: string; 298 | limit?: number; 299 | start?: number; 300 | q?: string; 301 | dateStart?: string; 302 | dateEnd?: string; 303 | } 304 | 305 | /** 306 | * NPS API Client class 307 | */ 308 | class NPSApiClient { 309 | private api: AxiosInstance; 310 | private baseUrl: string = 'https://developer.nps.gov/api/v1'; 311 | private apiKey: string; 312 | 313 | constructor() { 314 | this.apiKey = process.env.NPS_API_KEY || ''; 315 | 316 | if (!this.apiKey) { 317 | console.warn('Warning: NPS_API_KEY is not set in environment variables.'); 318 | console.warn('Get your API key at: https://www.nps.gov/subjects/developer/get-started.htm'); 319 | } 320 | 321 | // Create axios instance for NPS API 322 | this.api = axios.create({ 323 | baseURL: this.baseUrl, 324 | headers: { 325 | 'X-Api-Key': this.apiKey, 326 | }, 327 | }); 328 | 329 | // Add response interceptor for error handling 330 | this.api.interceptors.response.use( 331 | (response) => response, 332 | (error) => { 333 | if (error.response) { 334 | // Check for rate limiting 335 | if (error.response.status === 429) { 336 | console.error('Rate limit exceeded for NPS API. Please try again later.'); 337 | } 338 | 339 | // Log the error details 340 | console.error('NPS API Error:', { 341 | status: error.response.status, 342 | statusText: error.response.statusText, 343 | data: error.response.data, 344 | }); 345 | } else if (error.request) { 346 | console.error('No response received from NPS API:', error.request); 347 | } else { 348 | console.error('Error setting up NPS API request:', error.message); 349 | } 350 | 351 | return Promise.reject(error); 352 | } 353 | ); 354 | } 355 | 356 | /** 357 | * Fetch parks data from the NPS API 358 | * @param params Query parameters 359 | * @returns Promise with parks data 360 | */ 361 | async getParks(params: ParkQueryParams = {}): Promise<NPSResponse<ParkData>> { 362 | try { 363 | const response = await this.api.get('/parks', { params }); 364 | return response.data; 365 | } catch (error) { 366 | console.error('Error fetching parks data:', error); 367 | throw error; 368 | } 369 | } 370 | 371 | /** 372 | * Fetch a specific park by its parkCode 373 | * @param parkCode The park code (e.g., 'yose' for Yosemite) 374 | * @returns Promise with the park data 375 | */ 376 | async getParkByCode(parkCode: string): Promise<NPSResponse<ParkData>> { 377 | try { 378 | const response = await this.api.get('/parks', { 379 | params: { 380 | parkCode, 381 | limit: 1 382 | } 383 | }); 384 | return response.data; 385 | } catch (error) { 386 | console.error(`Error fetching park with code ${parkCode}:`, error); 387 | throw error; 388 | } 389 | } 390 | 391 | /** 392 | * Fetch alerts from the NPS API 393 | * @param params Query parameters 394 | * @returns Promise with alerts data 395 | */ 396 | async getAlerts(params: AlertQueryParams = {}): Promise<NPSResponse<AlertData>> { 397 | try { 398 | const response = await this.api.get('/alerts', { params }); 399 | return response.data; 400 | } catch (error) { 401 | console.error('Error fetching alerts data:', error); 402 | throw error; 403 | } 404 | } 405 | 406 | /** 407 | * Fetch alerts for a specific park 408 | * @param parkCode The park code (e.g., 'yose' for Yosemite) 409 | * @returns Promise with the park's alerts 410 | */ 411 | async getAlertsByParkCode(parkCode: string): Promise<NPSResponse<AlertData>> { 412 | try { 413 | const response = await this.api.get('/alerts', { 414 | params: { 415 | parkCode 416 | } 417 | }); 418 | return response.data; 419 | } catch (error) { 420 | console.error(`Error fetching alerts for park ${parkCode}:`, error); 421 | throw error; 422 | } 423 | } 424 | 425 | /** 426 | * Fetch visitor centers from the NPS API 427 | * @param params Query parameters 428 | * @returns Promise with visitor centers data 429 | */ 430 | async getVisitorCenters(params: VisitorCenterQueryParams = {}): Promise<NPSResponse<VisitorCenterData>> { 431 | try { 432 | const response = await this.api.get('/visitorcenters', { params }); 433 | return response.data; 434 | } catch (error) { 435 | console.error('Error fetching visitor centers data:', error); 436 | throw error; 437 | } 438 | } 439 | 440 | /** 441 | * Fetch campgrounds from the NPS API 442 | * @param params Query parameters 443 | * @returns Promise with campgrounds data 444 | */ 445 | async getCampgrounds(params: CampgroundQueryParams = {}): Promise<NPSResponse<CampgroundData>> { 446 | try { 447 | const response = await this.api.get('/campgrounds', { params }); 448 | return response.data; 449 | } catch (error) { 450 | console.error('Error fetching campgrounds data:', error); 451 | throw error; 452 | } 453 | } 454 | 455 | /** 456 | * Fetch events from the NPS API 457 | * @param params Query parameters 458 | * @returns Promise with events data 459 | */ 460 | async getEvents(params: EventQueryParams = {}): Promise<NPSResponse<EventData>> { 461 | try { 462 | const response = await this.api.get('/events', { params }); 463 | return response.data; 464 | } catch (error) { 465 | console.error('Error fetching events data:', error); 466 | throw error; 467 | } 468 | } 469 | } 470 | 471 | // Export a singleton instance 472 | export const npsApiClient = new NPSApiClient(); ``` -------------------------------------------------------------------------------- /src/formatters.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ParkData, AlertData, VisitorCenterData, CampgroundData, EventData } from './utils/npsApiClient.js'; 2 | 3 | /** 4 | * Format the park data into a more readable format for LLMs 5 | */ 6 | export function formatParkData(parkData: ParkData[]) { 7 | return parkData.map(park => ({ 8 | name: park.fullName, 9 | code: park.parkCode, 10 | description: park.description, 11 | states: park.states.split(',').map(code => code.trim()), 12 | url: park.url, 13 | designation: park.designation, 14 | activities: park.activities.map(activity => activity.name), 15 | weatherInfo: park.weatherInfo, 16 | location: { 17 | latitude: park.latitude, 18 | longitude: park.longitude 19 | }, 20 | entranceFees: park.entranceFees.map(fee => ({ 21 | cost: fee.cost, 22 | description: fee.description, 23 | title: fee.title 24 | })), 25 | operatingHours: park.operatingHours.map(hours => ({ 26 | name: hours.name, 27 | description: hours.description, 28 | standardHours: hours.standardHours 29 | })), 30 | contacts: { 31 | phoneNumbers: park.contacts.phoneNumbers.map(phone => ({ 32 | type: phone.type, 33 | number: phone.phoneNumber, 34 | description: phone.description 35 | })), 36 | emailAddresses: park.contacts.emailAddresses.map(email => ({ 37 | address: email.emailAddress, 38 | description: email.description 39 | })) 40 | }, 41 | images: park.images.map(image => ({ 42 | url: image.url, 43 | title: image.title, 44 | altText: image.altText, 45 | caption: image.caption, 46 | credit: image.credit 47 | })) 48 | })); 49 | } 50 | 51 | /** 52 | * Format park details for a single park 53 | */ 54 | export function formatParkDetails(park: ParkData) { 55 | // Determine the best address to use as the primary address 56 | const physicalAddress = park.addresses.find(addr => addr.type === 'Physical') || park.addresses[0]; 57 | 58 | // Format operating hours in a more readable way 59 | const formattedHours = park.operatingHours.map(hours => { 60 | const { standardHours } = hours; 61 | const formattedStandardHours = Object.entries(standardHours) 62 | .map(([day, hours]) => { 63 | // Convert day to proper case (e.g., 'monday' to 'Monday') 64 | const properDay = day.charAt(0).toUpperCase() + day.slice(1); 65 | return `${properDay}: ${hours || 'Closed'}`; 66 | }); 67 | 68 | return { 69 | name: hours.name, 70 | description: hours.description, 71 | standardHours: formattedStandardHours 72 | }; 73 | }); 74 | 75 | return { 76 | name: park.fullName, 77 | code: park.parkCode, 78 | url: park.url, 79 | description: park.description, 80 | designation: park.designation, 81 | states: park.states.split(',').map(code => code.trim()), 82 | weatherInfo: park.weatherInfo, 83 | directionsInfo: park.directionsInfo, 84 | directionsUrl: park.directionsUrl, 85 | location: { 86 | latitude: park.latitude, 87 | longitude: park.longitude, 88 | address: physicalAddress ? { 89 | line1: physicalAddress.line1, 90 | line2: physicalAddress.line2, 91 | city: physicalAddress.city, 92 | stateCode: physicalAddress.stateCode, 93 | postalCode: physicalAddress.postalCode 94 | } : undefined 95 | }, 96 | contacts: { 97 | phoneNumbers: park.contacts.phoneNumbers.map(phone => ({ 98 | type: phone.type, 99 | number: phone.phoneNumber, 100 | extension: phone.extension, 101 | description: phone.description 102 | })), 103 | emailAddresses: park.contacts.emailAddresses.map(email => ({ 104 | address: email.emailAddress, 105 | description: email.description 106 | })) 107 | }, 108 | entranceFees: park.entranceFees.map(fee => ({ 109 | title: fee.title, 110 | cost: `$${fee.cost}`, 111 | description: fee.description 112 | })), 113 | entrancePasses: park.entrancePasses.map(pass => ({ 114 | title: pass.title, 115 | cost: `$${pass.cost}`, 116 | description: pass.description 117 | })), 118 | operatingHours: formattedHours, 119 | topics: park.topics.map(topic => topic.name), 120 | activities: park.activities.map(activity => activity.name), 121 | images: park.images.map(image => ({ 122 | url: image.url, 123 | title: image.title, 124 | altText: image.altText, 125 | caption: image.caption, 126 | credit: image.credit 127 | })) 128 | }; 129 | } 130 | 131 | /** 132 | * Format the alert data into a more readable format for LLMs 133 | */ 134 | export function formatAlertData(alertData: AlertData[]) { 135 | return alertData.map(alert => { 136 | // Get the date part from the lastIndexedDate (which is in ISO format) 137 | const lastUpdated = alert.lastIndexedDate ? new Date(alert.lastIndexedDate).toLocaleDateString() : 'Unknown'; 138 | 139 | // Categorize the alert type 140 | let alertType = alert.category; 141 | if (alertType === 'Information') { 142 | alertType = 'Information (non-emergency)'; 143 | } else if (alertType === 'Caution') { 144 | alertType = 'Caution (potential hazard)'; 145 | } else if (alertType === 'Danger') { 146 | alertType = 'Danger (significant hazard)'; 147 | } else if (alertType === 'Park Closure') { 148 | alertType = 'Park Closure (area inaccessible)'; 149 | } 150 | 151 | return { 152 | title: alert.title, 153 | description: alert.description, 154 | parkCode: alert.parkCode, 155 | type: alertType, 156 | url: alert.url, 157 | lastUpdated 158 | }; 159 | }); 160 | } 161 | 162 | /** 163 | * Format visitor center data for better readability 164 | */ 165 | export function formatVisitorCenterData(visitorCenterData: VisitorCenterData[]) { 166 | return visitorCenterData.map(center => { 167 | // Find physical address if available 168 | const physicalAddress = center.addresses.find(addr => addr.type === 'Physical') || center.addresses[0]; 169 | 170 | // Format operating hours 171 | const formattedHours = center.operatingHours.map(hours => { 172 | const { standardHours } = hours; 173 | const formattedStandardHours = Object.entries(standardHours) 174 | .map(([day, hours]) => { 175 | // Convert day to proper case (e.g., 'monday' to 'Monday') 176 | const properDay = day.charAt(0).toUpperCase() + day.slice(1); 177 | return `${properDay}: ${hours || 'Closed'}`; 178 | }); 179 | 180 | return { 181 | name: hours.name, 182 | description: hours.description, 183 | standardHours: formattedStandardHours 184 | }; 185 | }); 186 | 187 | return { 188 | name: center.name, 189 | parkCode: center.parkCode, 190 | description: center.description, 191 | url: center.url, 192 | directionsInfo: center.directionsInfo, 193 | directionsUrl: center.directionsUrl, 194 | location: { 195 | latitude: center.latitude, 196 | longitude: center.longitude, 197 | address: physicalAddress ? { 198 | line1: physicalAddress.line1, 199 | line2: physicalAddress.line2, 200 | city: physicalAddress.city, 201 | stateCode: physicalAddress.stateCode, 202 | postalCode: physicalAddress.postalCode 203 | } : undefined 204 | }, 205 | operatingHours: formattedHours, 206 | contacts: { 207 | phoneNumbers: center.contacts.phoneNumbers.map(phone => ({ 208 | type: phone.type, 209 | number: phone.phoneNumber, 210 | extension: phone.extension, 211 | description: phone.description 212 | })), 213 | emailAddresses: center.contacts.emailAddresses.map(email => ({ 214 | address: email.emailAddress, 215 | description: email.description 216 | })) 217 | } 218 | }; 219 | }); 220 | } 221 | 222 | /** 223 | * Format campground data for better readability 224 | */ 225 | export function formatCampgroundData(campgroundData: CampgroundData[]) { 226 | return campgroundData.map(campground => { 227 | // Find physical address if available 228 | const physicalAddress = campground.addresses.find(addr => addr.type === 'Physical') || campground.addresses[0]; 229 | 230 | // Format operating hours 231 | const formattedHours = campground.operatingHours.map(hours => { 232 | const { standardHours } = hours; 233 | const formattedStandardHours = Object.entries(standardHours) 234 | .map(([day, hours]) => { 235 | const properDay = day.charAt(0).toUpperCase() + day.slice(1); 236 | return `${properDay}: ${hours || 'Closed'}`; 237 | }); 238 | 239 | return { 240 | name: hours.name, 241 | description: hours.description, 242 | standardHours: formattedStandardHours 243 | }; 244 | }); 245 | 246 | // Format amenities for better readability 247 | const amenities = []; 248 | if (campground.amenities) { 249 | if (campground.amenities.trashRecyclingCollection) amenities.push('Trash/Recycling Collection'); 250 | if (campground.amenities.toilets && campground.amenities.toilets.length > 0) 251 | amenities.push(`Toilets (${campground.amenities.toilets.join(', ')})`); 252 | if (campground.amenities.internetConnectivity) amenities.push('Internet Connectivity'); 253 | if (campground.amenities.showers && campground.amenities.showers.length > 0) 254 | amenities.push(`Showers (${campground.amenities.showers.join(', ')})`); 255 | if (campground.amenities.cellPhoneReception) amenities.push('Cell Phone Reception'); 256 | if (campground.amenities.laundry) amenities.push('Laundry'); 257 | if (campground.amenities.amphitheater) amenities.push('Amphitheater'); 258 | if (campground.amenities.dumpStation) amenities.push('Dump Station'); 259 | if (campground.amenities.campStore) amenities.push('Camp Store'); 260 | if (campground.amenities.staffOrVolunteerHostOnsite) amenities.push('Staff/Volunteer Host Onsite'); 261 | if (campground.amenities.potableWater && campground.amenities.potableWater.length > 0) 262 | amenities.push(`Potable Water (${campground.amenities.potableWater.join(', ')})`); 263 | if (campground.amenities.iceAvailableForSale) amenities.push('Ice Available For Sale'); 264 | if (campground.amenities.firewoodForSale) amenities.push('Firewood For Sale'); 265 | if (campground.amenities.foodStorageLockers) amenities.push('Food Storage Lockers'); 266 | } 267 | 268 | return { 269 | name: campground.name, 270 | parkCode: campground.parkCode, 271 | description: campground.description, 272 | url: campground.url, 273 | reservationInfo: campground.reservationInfo, 274 | reservationUrl: campground.reservationUrl, 275 | regulations: campground.regulationsOverview, 276 | regulationsUrl: campground.regulationsurl, 277 | weatherOverview: campground.weatherOverview, 278 | location: { 279 | latitude: campground.latitude, 280 | longitude: campground.longitude, 281 | address: physicalAddress ? { 282 | line1: physicalAddress.line1, 283 | line2: physicalAddress.line2, 284 | city: physicalAddress.city, 285 | stateCode: physicalAddress.stateCode, 286 | postalCode: physicalAddress.postalCode 287 | } : undefined 288 | }, 289 | operatingHours: formattedHours, 290 | fees: campground.fees.map(fee => ({ 291 | title: fee.title, 292 | cost: `$${fee.cost}`, 293 | description: fee.description 294 | })), 295 | totalSites: campground.campsites?.totalSites || '0', 296 | sitesReservable: campground.numberOfSitesReservable || '0', 297 | sitesFirstComeFirstServe: campground.numberOfSitesFirstComeFirstServe || '0', 298 | campsiteTypes: { 299 | group: campground.campsites?.group || '0', 300 | horse: campground.campsites?.horse || '0', 301 | tentOnly: campground.campsites?.tentOnly || '0', 302 | electricalHookups: campground.campsites?.electricalHookups || '0', 303 | rvOnly: campground.campsites?.rvOnly || '0', 304 | walkBoatTo: campground.campsites?.walkBoatTo || '0', 305 | other: campground.campsites?.other || '0' 306 | }, 307 | amenities: amenities, 308 | accessibility: { 309 | wheelchairAccess: campground.accessibility?.wheelchairAccess, 310 | rvAllowed: campground.accessibility?.rvAllowed, 311 | rvMaxLength: campground.accessibility?.rvMaxLength, 312 | trailerAllowed: campground.accessibility?.trailerAllowed, 313 | trailerMaxLength: campground.accessibility?.trailerMaxLength, 314 | accessRoads: campground.accessibility?.accessRoads, 315 | adaInfo: campground.accessibility?.adaInfo 316 | }, 317 | contacts: { 318 | phoneNumbers: campground.contacts.phoneNumbers.map(phone => ({ 319 | type: phone.type, 320 | number: phone.phoneNumber, 321 | extension: phone.extension, 322 | description: phone.description 323 | })), 324 | emailAddresses: campground.contacts.emailAddresses.map(email => ({ 325 | address: email.emailAddress, 326 | description: email.description 327 | })) 328 | } 329 | }; 330 | }); 331 | } 332 | 333 | /** 334 | * Format event data for better readability 335 | */ 336 | export function formatEventData(eventData: EventData[]) { 337 | return eventData.map(event => { 338 | // Format dates and times 339 | const formattedDates = event.dates ? event.dates.join(', ') : ''; 340 | 341 | // Format times 342 | const formattedTimes = event.times.map(time => { 343 | let timeString = ''; 344 | if (time.timeStart) { 345 | timeString += time.sunriseTimeStart ? 'Sunrise' : time.timeStart; 346 | } 347 | if (time.timeEnd) { 348 | timeString += ' to '; 349 | timeString += time.sunsetTimeEnd ? 'Sunset' : time.timeEnd; 350 | } 351 | return timeString || 'All day'; 352 | }).join(', '); 353 | 354 | return { 355 | title: event.title, 356 | parkCode: event.parkCode, 357 | parkName: event.parkFullName, 358 | description: event.description, 359 | category: event.category, 360 | subcategory: event.subcategory, 361 | tags: event.tags, 362 | location: event.location, 363 | coordinates: { 364 | latitude: event.latitude, 365 | longitude: event.longitude 366 | }, 367 | dateTime: { 368 | dates: formattedDates, 369 | times: formattedTimes, 370 | dateStart: event.dateStart, 371 | dateEnd: event.dateEnd, 372 | isAllDay: event.isAllDay, 373 | isRecurring: event.isRecurring, 374 | recurrenceDateStart: event.recurrenceDateStart, 375 | recurrenceDateEnd: event.recurrenceDateEnd 376 | }, 377 | feeInfo: event.feeInfo, 378 | contactInfo: { 379 | email: event.contactEmailAddress, 380 | phone: event.contactTelephoneNumber 381 | }, 382 | infoUrl: event.infoURL || event.url, 383 | lastUpdated: event.lastUpdated 384 | }; 385 | }); 386 | } ```