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