Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/weather-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
public/
*.log
.env
.env.local
99 changes: 99 additions & 0 deletions examples/weather-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Weather App Example

An MCP application demonstrating weather tools built with @mcp-apps-kit. Uses the [Open-Meteo API](https://open-meteo.com/) for real weather data (free, no API key required) with mock data fallback.

## Features

- **getCurrentWeather**: Get current weather conditions for any location
- **getForecast**: Get up to 16-day weather forecast
- **getWeatherAlerts**: Get weather alerts and warnings (mock data)

## Tools

| Tool | Description |
| ------------------- | ------------------------------------------------------------------------- |
| `getCurrentWeather` | Returns current temperature, humidity, wind, and conditions |
| `getForecast` | Returns daily forecast with high/low temps, precipitation, sunrise/sunset |
| `getWeatherAlerts` | Returns active weather warnings, watches, and advisories |

## Development

```bash
# Install dependencies (from monorepo root)
pnpm install

# Run development server
pnpm -C examples/weather-app dev
```

## Build

```bash
pnpm -C examples/weather-app build
```

## Testing

```bash
# Run tests
pnpm -C examples/weather-app test

# Run tests in watch mode
pnpm -C examples/weather-app test:watch
```

### Mock Mode

Set `USE_MOCK_WEATHER=true` to use mock data instead of the real API:

```bash
USE_MOCK_WEATHER=true pnpm -C examples/weather-app dev
```

## Connecting to an MCP Apps Host

Configure your MCP Apps-compatible host to connect to the server:

**HTTP mode (default):**

- Endpoint: `http://localhost:3005/mcp`

**Stdio mode (for hosts that support it):**

```bash
npx tsx examples/weather-app/server/index.ts
```

## Project Structure

```
weather-app/
├── server/
│ ├── index.ts # App entry point with tool definitions
│ └── services/
│ └── weatherService.ts # Open-Meteo API integration
├── ui/
│ ├── src/
│ │ ├── App.tsx # React UI components
│ │ └── styles.css # Styling
│ └── vite.config.ts
└── tests/
├── integration/
│ └── server.test.ts # Integration tests
└── unit/
└── weatherService.test.ts # Unit tests
```

## API Reference

### Open-Meteo

This example uses the [Open-Meteo API](https://open-meteo.com/), a free and open-source weather API that doesn't require authentication.

- **Geocoding**: Converts city names to coordinates
- **Weather**: Current conditions and forecasts
- **Limits**: No rate limits for reasonable usage

## License

MIT
43 changes: 43 additions & 0 deletions examples/weather-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@mcp-apps-kit/example-weather-app",
"version": "0.1.0",
"private": true,
"description": "Weather app example demonstrating Open-Meteo API integration with @mcp-apps-kit/core",
"type": "module",
"scripts": {
"dev": "concurrently \"pnpm dev:server\" \"pnpm dev:ui\"",
"dev:server": "tsx watch server/index.ts",
"dev:ui": "vite build --watch --config ui/vite.config.ts",
"start": "tsx server/index.ts",
"build": "pnpm build:ui && tsc",
"build:ui": "vite build --config ui/vite.config.ts",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@mcp-apps-kit/core": "workspace:*",
"@mcp-apps-kit/ui": "workspace:*",
"@mcp-apps-kit/ui-react": "workspace:*",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"zod": "^4.0.0"
},
"devDependencies": {
"@mcp-apps-kit/testing": "workspace:*",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"concurrently": "^9.2.1",
"jsdom": "^27.3.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^7.3.0",
"vite-plugin-singlefile": "^2.3.0",
"vitest": "^4.0.16"
}
}
204 changes: 204 additions & 0 deletions examples/weather-app/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* Weather App - MCP Server
*
* Demonstrates weather-related tools using Open-Meteo API (free, no API key required)
* with mock data fallback for testing and offline usage.
*/

import { createApp, defineTool, defineUI, type ClientToolsFromCore } from "@mcp-apps-kit/core";
import { z } from "zod";
import {
WeatherService,
type CurrentWeather,
type WeatherForecast,
type WeatherAlertsResponse,
} from "./services/weatherService.js";

// Initialize weather service (uses real API by default, falls back to mock)
const weatherService = new WeatherService({
useMock: process.env.USE_MOCK_WEATHER === "true",
});

// Define UIs for weather widgets
const currentWeatherUI = defineUI({
name: "Current Weather Widget",
description: "Displays current weather conditions",
html: "./ui/dist/index.html",
prefersBorder: true,
});

const forecastUI = defineUI({
name: "Weather Forecast Widget",
description: "Displays weather forecast",
html: "./ui/dist/index.html",
prefersBorder: true,
});

const alertsUI = defineUI({
name: "Weather Alerts Widget",
description: "Displays weather alerts and warnings",
html: "./ui/dist/index.html",
prefersBorder: true,
});

// Schema definitions
const currentWeatherOutputSchema = z.object({
location: z.object({
name: z.string(),
latitude: z.number(),
longitude: z.number(),
country: z.string().optional(),
timezone: z.string().optional(),
}),
temperature: z.number().describe("Temperature in Celsius"),
feelsLike: z.number().describe("Feels like temperature in Celsius"),
humidity: z.number().describe("Relative humidity percentage"),
windSpeed: z.number().describe("Wind speed in km/h"),
windDirection: z.number().describe("Wind direction in degrees"),
weatherCode: z.number(),
description: z.string(),
icon: z.string(),
isDay: z.boolean(),
timestamp: z.string(),
});

const dailyForecastSchema = z.object({
date: z.string(),
temperatureMax: z.number(),
temperatureMin: z.number(),
weatherCode: z.number(),
description: z.string(),
icon: z.string(),
precipitationProbability: z.number(),
windSpeedMax: z.number(),
sunrise: z.string(),
sunset: z.string(),
});

const forecastOutputSchema = z.object({
location: z.object({
name: z.string(),
latitude: z.number(),
longitude: z.number(),
country: z.string().optional(),
timezone: z.string().optional(),
}),
daily: z.array(dailyForecastSchema),
generatedAt: z.string(),
});

const alertSchema = z.object({
id: z.string(),
type: z.enum(["warning", "watch", "advisory"]),
severity: z.enum(["minor", "moderate", "severe", "extreme"]),
headline: z.string(),
description: z.string(),
startTime: z.string(),
endTime: z.string(),
});

const alertsOutputSchema = z.object({
location: z.object({
name: z.string(),
latitude: z.number(),
longitude: z.number(),
country: z.string().optional(),
timezone: z.string().optional(),
}),
alerts: z.array(alertSchema),
lastChecked: z.string(),
});

// Create the MCP App
const app = createApp({
name: "weather-app",
version: "0.1.0",

config: {
protocol: "openai",
},

tools: {
getCurrentWeather: defineTool({
title: "Get Current Weather",
description:
"Get the current weather conditions for a specified location. Returns temperature, humidity, wind, and conditions.",
input: z.object({
location: z
.string()
.describe(
"City name, address, or location to get weather for (e.g., 'New York', 'Tokyo', 'London, UK')"
),
}),
output: currentWeatherOutputSchema,
visibility: "both",
annotations: {
readOnlyHint: true,
},
handler: async ({ location }): Promise<CurrentWeather> => {
return await weatherService.getCurrentWeather(location);
},
ui: currentWeatherUI,
}),

getForecast: defineTool({
title: "Get Weather Forecast",
description:
"Get a multi-day weather forecast for a specified location. Returns daily forecasts with temperatures, precipitation, and conditions.",
input: z.object({
location: z.string().describe("City name, address, or location to get forecast for"),
days: z
.number()
.min(1)
.max(16)
.default(7)
.describe("Number of days to forecast (1-16, default: 7)"),
}),
output: forecastOutputSchema,
visibility: "both",
annotations: {
readOnlyHint: true,
},
handler: async ({ location, days }): Promise<WeatherForecast> => {
return await weatherService.getForecast(location, days);
},
ui: forecastUI,
}),

getWeatherAlerts: defineTool({
title: "Get Weather Alerts",
description:
"Get active weather alerts and warnings for a specified location. Returns any watches, warnings, or advisories in effect.",
input: z.object({
location: z.string().describe("City name, address, or location to check for alerts"),
}),
output: alertsOutputSchema,
visibility: "both",
annotations: {
readOnlyHint: true,
},
handler: async ({ location }): Promise<WeatherAlertsResponse> => {
return await weatherService.getAlerts(location);
},
ui: alertsUI,
}),
},
});

// Export types for UI components
export type AppToolsDef = {
getCurrentWeather: typeof app.tools.getCurrentWeather;
getForecast: typeof app.tools.getForecast;
getWeatherAlerts: typeof app.tools.getWeatherAlerts;
};
export type AppTools = ClientToolsFromCore<AppToolsDef>;

// Start server (skip in test environment)
if (process.env.NODE_ENV !== "test") {
const port = parseInt(process.env.PORT || "3005", 10);
await app.start({ port });
console.log(`Weather App MCP server running on http://localhost:${port}`);
}
Comment on lines +197 to +201

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate PORT environment variable to handle invalid values.

The parseInt call can return NaN if PORT contains a non-numeric string. While unlikely in practice, this could cause confusing startup errors.

🛡️ Suggested PORT validation
-  const port = parseInt(process.env.PORT || "3005", 10);
+  const portEnv = process.env.PORT || "3005";
+  const port = parseInt(portEnv, 10);
+  if (isNaN(port) || port < 1 || port > 65535) {
+    throw new Error(`Invalid PORT: ${portEnv}. Must be a number between 1 and 65535.`);
+  }
   await app.start({ port });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (process.env.NODE_ENV !== "test") {
const port = parseInt(process.env.PORT || "3005", 10);
await app.start({ port });
console.log(`Weather App MCP server running on http://localhost:${port}`);
}
if (process.env.NODE_ENV !== "test") {
const portEnv = process.env.PORT || "3005";
const port = parseInt(portEnv, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid PORT: ${portEnv}. Must be a number between 1 and 65535.`);
}
await app.start({ port });
console.log(`Weather App MCP server running on http://localhost:${port}`);
}
🤖 Prompt for AI Agents
In @examples/weather-app/server/index.ts around lines 197 - 201, The PORT
parsing can yield NaN for non-numeric strings; validate process.env.PORT after
parseInt and before calling app.start so you never pass NaN to app.start({ port
}). In the NODE_ENV !== "test" block, check the parsed port (the local variable
port) with Number.isInteger/Number.isFinite or isNaN, and if invalid either fall
back to the default (e.g., 3005) or log an error and exit; ensure any chosen
behavior logs the invalid process.env.PORT value to aid debugging before calling
app.start({ port }).


// Export app for testing
export { app };
Loading