# 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 | }
```