#
tokens: 17693/50000 19/19 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![smithery badge](https://smithery.ai/badge/@KyrieTangSheng/mcp-server-nationalparks)](https://smithery.ai/server/@KyrieTangSheng/mcp-server-nationalparks)
  3 | [![Verified on MseeP](https://mseep.ai/badge.svg)](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 | } 
```