diff --git a/images/available-mcp-tools.png b/images/available-mcp-tools.png new file mode 100644 index 0000000..89a2b9c Binary files /dev/null and b/images/available-mcp-tools.png differ diff --git a/images/current-weather.png b/images/current-weather.png new file mode 100644 index 0000000..7b440f1 Binary files /dev/null and b/images/current-weather.png differ diff --git a/images/quickstart-weather.png b/images/quickstart-weather.png deleted file mode 100644 index cee70d2..0000000 Binary files a/images/quickstart-weather.png and /dev/null differ diff --git a/images/time-server-quickstart.png b/images/time-server-quickstart.png deleted file mode 100644 index f48263b..0000000 Binary files a/images/time-server-quickstart.png and /dev/null differ diff --git a/images/visual-indicator-mcp-tools.png b/images/visual-indicator-mcp-tools.png new file mode 100644 index 0000000..af71d07 Binary files /dev/null and b/images/visual-indicator-mcp-tools.png differ diff --git a/images/weather-alerts.png b/images/weather-alerts.png new file mode 100644 index 0000000..d234ac5 Binary files /dev/null and b/images/weather-alerts.png differ diff --git a/quickstart.mdx b/quickstart.mdx index 341ae62..734fa39 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -1,11 +1,9 @@ --- title: "Quickstart" -description: "Get started with building your first MCP server and connecting it to an MCP host" +description: "Get started with building your first MCP server and connecting it to a host" --- -In this tutorial, we'll build a simple MCP server with a `get_weather` tool and connect it to a popular MCP host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases. - -You can think of MCP as a bridge between your LLMs and your data and tools. Model Context Protocol was developed by Anthropic (the makers of [Claude](https://claude.ai)), but MCP can work with any Large Language Model provider that supports tool calling. +In this tutorial, we'll build a simple MCP weather server and connect it to a host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases. ### What is MCP? @@ -18,13 +16,20 @@ MCP is an open protocol that standardizes how applications provide context to LL ### What we'll be building -Many LLMs (including Claude) do not currently have the ability to fetch the current temperature and weather conditions. Let's use MCP to solve that! +Many LLMs (including Claude) do not currently have the ability to fetch the forecast and severe weather alerts. Let's use MCP to solve that! + +We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, Claude Desktop): - + + + + -Servers can connect to any host. We'll walk through using Claude Desktop as the host and eventually show you how to build your own. + +Servers can connect to any host. We've chosen Claude desktop here for simplicity, but we also have guides on [building your own client](/tutorials/building-a-client) + Because servers are locally run, MCP currently only supports desktop hosts. Remote hosts are in active development. @@ -50,7 +55,7 @@ It also helps to be familiar with [tool use (aka function calling)](https://docs ### System requirements -For Python, make sure you have the latest version of Python installed. For TypeScript, make sure you have the latest version of Node installed. +For Python, make sure you have Python 3.9 or higher installed. For TypeScript, make sure you have the latest version of Node installed. ### Set up your environment @@ -64,11 +69,14 @@ First, let's install `uv` and set up our Python project and environment: curl -LsSf https://astral.sh/uv/install.sh | sh ``` -```bash Windows +```powershell Windows powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` + +Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up. + First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/). @@ -98,17 +106,16 @@ source .venv/bin/activate # Install dependencies uv pip install mcp httpx -# Clean up boilerplate code -rm -rf hello.py +# Remove template file +rm hello.py # Create our files -mkdir src -mkdir src/weather +mkdir -p src/weather touch src/weather/__init__.py touch src/weather/server.py ``` -```bash Windows +```powershell Windows # Create a new directory for our project uv init mcp-quickstart cd mcp-quickstart @@ -121,13 +128,13 @@ uv venv uv pip install mcp httpx # Clean up boilerplate code -del /f /q hello.py +rm hello.py # Create our files md src md src\weather -echo.> src\__init__.py -echo.> src\server.py +new-item src\weather\__init__.py +new-item src\weather\server.py ``` @@ -160,7 +167,9 @@ __all__ = ['main', 'server'] -```bash + + +```bash MacOS/Linux # Create a new directory for our project mkdir mcp-quickstart cd mcp-quickstart @@ -169,23 +178,39 @@ cd mcp-quickstart npm init -y # Install dependencies -npm install @modelcontextprotocol/sdk node-fetch +npm install @modelcontextprotocol/sdk zod npm install -D @types/node typescript -``` -Then create a new file called `src/index.ts` in your project directory: - -```bash +# Create our files mkdir src touch src/index.ts ``` + +```powershell Windows +# Create a new directory for our project +md mcp-quickstart +cd mcp-quickstart + +# Initialize a new npm project +npm init -y + +# Install dependencies +npm install @modelcontextprotocol/sdk zod +npm install -D @types/node typescript + +# Create our files +md src +new-item src\index.ts +``` + + Update your package.json to add type: "module" and a build script: ```json package.json { "type": "module", "bin": { - "mcp-quickstart": "./build/index.js" + "weather": "./build/index.js" }, "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", @@ -196,7 +221,7 @@ Update your package.json to add type: "module" and a build script: } ``` -Create a tsconfig.json: +Create a `tsconfig.json` in the root of your project: ```json tsconfig.json { @@ -224,113 +249,563 @@ Now let's dive into building your server. ### Importing packages -Add these to your server.py: + + +Add these to the top of your `server.py`: ```python from typing import Any + import httpx from mcp.server.models import InitializationOptions import mcp.types as types from mcp.server import NotificationOptions, Server import mcp.server.stdio ``` + + +Add these to the top of your `src/index.ts`: + +```typescript +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +``` + + ### Setting up the instance -Then add the cache, server instance and the base URL for the NWS API: + + +Then initialize the server instance and the base URL for the NWS API: ```python -# Cache weather data -weather_cache: dict[str, Any] = {} NWS_API_BASE = "https://api.weather.gov" +USER_AGENT = "weather-app/1.0" server = Server("weather") ``` + + +Then initialize the NWS API base URL, validation schemas, and server instance: + +```typescript +const NWS_API_BASE = "https://api.weather.gov"; +const USER_AGENT = "weather-app/1.0"; + +// Define Zod schemas for validation +const AlertsArgumentsSchema = z.object({ + state: z.string().length(2), +}); + +const ForecastArgumentsSchema = z.object({ + latitude: z.number().min(-90).max(90), + longitude: z.number().min(-180).max(180), +}); + +// Create server instance +const server = new Server( + { + name: "weather", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } +); +``` + + ### Implementing tool listing + + We need to tell clients what tools are available. The `list_tools()` decorator registers this handler: ```python @server.list_tools() async def handle_list_tools() -> list[types.Tool]: + """ + List available tools. + Each tool specifies its arguments using JSON Schema validation. + """ return [ types.Tool( - name="get-weather", - description="Get weather data for a location", + name="get-alerts", + description="Get weather alerts for a state", inputSchema={ "type": "object", "properties": { - "latitude": {"type": "number"}, - "longitude": {"type": "number"}, + "state": { + "type": "string", + "description": "Two-letter state code (e.g. CA, NY)", + }, + }, + "required": ["state"], + }, + ), + types.Tool( + name="get-forecast", + description="Get weather forecast for a location", + inputSchema={ + "type": "object", + "properties": { + "latitude": { + "type": "number", + "description": "Latitude of the location", + }, + "longitude": { + "type": "number", + "description": "Longitude of the location", + }, }, "required": ["latitude", "longitude"], }, - ) + ), ] -``` -This defines a single tool that requires latitude and longitude parameters. +``` + + +We need to tell clients what tools are available. This `server.setRequestHandler` call will register this list for us: + +```typescript +// List available tools +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "get-alerts", + description: "Get weather alerts for a state", + inputSchema: { + type: "object", + properties: { + state: { + type: "string", + description: "Two-letter state code (e.g. CA, NY)", + }, + }, + required: ["state"], + }, + }, + { + name: "get-forecast", + description: "Get weather forecast for a location", + inputSchema: { + type: "object", + properties: { + latitude: { + type: "number", + description: "Latitude of the location", + }, + longitude: { + type: "number", + description: "Longitude of the location", + }, + }, + required: ["latitude", "longitude"], + }, + }, + ], + }; +}); +``` + + + +This defines our two tools: `get-alerts` and `get-forecast`. + +### Helper functions + +Next, let's add our helper functions for querying and formatting the data from the National Weather Service API: + + + +```python +async def make_nws_request(client: httpx.AsyncClient, url: str) -> dict[str, Any] | None: + """Make a request to the NWS API with proper error handling.""" + headers = { + "User-Agent": USER_AGENT, + "Accept": "application/geo+json" + } + + try: + response = await client.get(url, headers=headers, timeout=30.0) + response.raise_for_status() + return response.json() + except Exception: + return None + +def format_alert(feature: dict) -> str: + """Format an alert feature into a concise string.""" + props = feature["properties"] + return ( + f"Event: {props.get('event', 'Unknown')}\n" + f"Area: {props.get('areaDesc', 'Unknown')}\n" + f"Severity: {props.get('severity', 'Unknown')}\n" + f"Status: {props.get('status', 'Unknown')}\n" + f"Headline: {props.get('headline', 'No headline')}\n" + "---" + ) +``` + + +```typescript +// Helper function for making NWS API requests +async function makeNWSRequest(url: string): Promise { + const headers = { + "User-Agent": USER_AGENT, + Accept: "application/geo+json", + }; + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return (await response.json()) as T; + } catch (error) { + console.error("Error making NWS request:", error); + return null; + } +} + +interface AlertFeature { + properties: { + event?: string; + areaDesc?: string; + severity?: string; + status?: string; + headline?: string; + }; +} + +// Format alert data +function formatAlert(feature: AlertFeature): string { + const props = feature.properties; + return [ + `Event: ${props.event || "Unknown"}`, + `Area: ${props.areaDesc || "Unknown"}`, + `Severity: ${props.severity || "Unknown"}`, + `Status: ${props.status || "Unknown"}`, + `Headline: ${props.headline || "No headline"}`, + "---", + ].join("\n"); +} + +interface ForecastPeriod { + name?: string; + temperature?: number; + temperatureUnit?: string; + windSpeed?: string; + windDirection?: string; + shortForecast?: string; +} + +interface AlertsResponse { + features: AlertFeature[]; +} + +interface PointsResponse { + properties: { + forecast?: string; + }; +} + +interface ForecastResponse { + properties: { + periods: ForecastPeriod[]; + }; +} +``` + + ### Implementing tool execution -The tool execution handler processes requests to get weather data: +The tool execution handler is responsible for actually executing the logic of each tool. Let's add it: + + ```python @server.call_tool() async def handle_call_tool( - name: str, - arguments: dict | None + name: str, arguments: dict | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: - # Validate tool name and arguments - if name != "get-weather": - raise ValueError(f"Unknown tool: {name}") + """ + Handle tool execution requests. + Tools can fetch weather data and notify clients of changes. + """ if not arguments: raise ValueError("Missing arguments") - - lat = arguments.get("latitude") - lon = arguments.get("longitude") - if lat is None or lon is None: - raise ValueError("Missing latitude or longitude") - - # Fetch weather data - async with httpx.AsyncClient() as client: - # Get grid point - points_url = f"{NWS_API_BASE}/points/{lat},{lon}" - points_response = await client.get( - points_url, - headers={"Accept": "application/json"} - ) - points_data = points_response.json() - - # Get forecast - forecast_url = points_data["properties"]["forecast"] - forecast_response = await client.get( - forecast_url, - headers={"Accept": "application/json"} - ) - forecast_data = forecast_response.json() + + if name == "get-alerts": + state = arguments.get("state") + if not state: + raise ValueError("Missing state parameter") - # Cache the forecast data - location_key = f"{lat},{lon}" - weather_cache[location_key] = forecast_data - - # Notify clients of resource changes - await server.request_context.session.send_resource_list_changed() - - return [ - types.TextContent( + # Convert state to uppercase to ensure consistent format + state = state.upper() + if len(state) != 2: + raise ValueError("State must be a two-letter code (e.g. CA, NY)") + + async with httpx.AsyncClient() as client: + alerts_url = f"{NWS_API_BASE}/alerts?area={state}" + alerts_data = await make_nws_request(client, alerts_url) + + if not alerts_data: + return [types.TextContent(type="text", text="Failed to retrieve alerts data")] + + features = alerts_data.get("features", []) + if not features: + return [types.TextContent(type="text", text=f"No active alerts for {state}")] + + # Format each alert into a concise string + formatted_alerts = [format_alert(feature) for feature in features] + alerts_text = f"Active alerts for {state}:\n\n" + "\n".join(formatted_alerts) + + return [ + types.TextContent( + type="text", + text=alerts_text + ) + ] + elif name == "get-forecast": + try: + latitude = float(arguments.get("latitude")) + longitude = float(arguments.get("longitude")) + except (TypeError, ValueError): + return [types.TextContent( type="text", - text=f"Retrieved weather data for location ({lat}, {lon})", - ) - ] + text="Invalid coordinates. Please provide valid numbers for latitude and longitude." + )] + + # Basic coordinate validation + if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180): + return [types.TextContent( + type="text", + text="Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180." + )] + + async with httpx.AsyncClient() as client: + # First get the grid point + lat_str = f"{latitude:.4f}" + lon_str = f"{longitude:.4f}" + points_url = f"{NWS_API_BASE}/points/{lat_str},{lon_str}" + points_data = await make_nws_request(client, points_url) + + if not points_data: + return [types.TextContent(type="text", text=f"Failed to retrieve grid point data for coordinates: {latitude}, {longitude}. This location may not be supported by the NWS API (only US locations are supported).")] + + # Extract forecast URL from the response + properties = points_data.get("properties", {}) + forecast_url = properties.get("forecast") + + if not forecast_url: + return [types.TextContent(type="text", text="Failed to get forecast URL from grid point data")] + + # Get the forecast + forecast_data = await make_nws_request(client, forecast_url) + + if not forecast_data: + return [types.TextContent(type="text", text="Failed to retrieve forecast data")] + + # Format the forecast periods + periods = forecast_data.get("properties", {}).get("periods", []) + if not periods: + return [types.TextContent(type="text", text="No forecast periods available")] + + # Format each period into a concise string + formatted_forecast = [] + for period in periods: + forecast_text = ( + f"{period.get('name', 'Unknown')}:\n" + f"Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}\n" + f"Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}\n" + f"{period.get('shortForecast', 'No forecast available')}\n" + "---" + ) + formatted_forecast.append(forecast_text) + + forecast_text = f"Forecast for {latitude}, {longitude}:\n\n" + "\n".join(formatted_forecast) + + return [types.TextContent( + type="text", + text=forecast_text + )] + else: + raise ValueError(f"Unknown tool: {name}") ``` + + + +```typescript +// Handle tool execution +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + if (name === "get-alerts") { + const { state } = AlertsArgumentsSchema.parse(args); + const stateCode = state.toUpperCase(); + + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; + const alertsData = await makeNWSRequest(alertsUrl); + + if (!alertsData) { + return { + content: [ + { + type: "text", + text: "Failed to retrieve alerts data", + }, + ], + }; + } + + const features = alertsData.features || []; + if (features.length === 0) { + return { + content: [ + { + type: "text", + text: `No active alerts for ${stateCode}`, + }, + ], + }; + } + + const formattedAlerts = features.map(formatAlert); + const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join( + "\n" + )}`; + + return { + content: [ + { + type: "text", + text: alertsText, + }, + ], + }; + } else if (name === "get-forecast") { + const { latitude, longitude } = ForecastArgumentsSchema.parse(args); + + // Get grid point data + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed( + 4 + )},${longitude.toFixed(4)}`; + const pointsData = await makeNWSRequest(pointsUrl); + + if (!pointsData) { + return { + content: [ + { + type: "text", + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, + }, + ], + }; + } + + const forecastUrl = pointsData.properties?.forecast; + if (!forecastUrl) { + return { + content: [ + { + type: "text", + text: "Failed to get forecast URL from grid point data", + }, + ], + }; + } + + // Get forecast data + const forecastData = await makeNWSRequest(forecastUrl); + if (!forecastData) { + return { + content: [ + { + type: "text", + text: "Failed to retrieve forecast data", + }, + ], + }; + } + + const periods = forecastData.properties?.periods || []; + if (periods.length === 0) { + return { + content: [ + { + type: "text", + text: "No forecast periods available", + }, + ], + }; + } + + // Format forecast periods + const formattedForecast = periods.map((period: ForecastPeriod) => + [ + `${period.name || "Unknown"}:`, + `Temperature: ${period.temperature || "Unknown"}°${ + period.temperatureUnit || "F" + }`, + `Wind: ${period.windSpeed || "Unknown"} ${ + period.windDirection || "" + }`, + `${period.shortForecast || "No forecast available"}`, + "---", + ].join("\n") + ); + + const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( + "\n" + )}`; + + return { + content: [ + { + type: "text", + text: forecastText, + }, + ], + }; + } else { + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error( + `Invalid arguments: ${error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join(", ")}` + ); + } + throw error; + } +}); +``` + + ### Running the server Finally, implement the main function to run the server: + + ```python async def main(): + # Run the server using stdin/stdout streams async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, @@ -345,6 +820,25 @@ async def main(): ), ) ``` + + +```typescript +// Start the server +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Weather MCP Server running on stdio"); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); +``` + +Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect. + + Your server is complete! Let's now test your server from an existing MCP host, Claude for Desktop. @@ -356,7 +850,7 @@ Unfortunately, Claude for Desktop is not yet available on Linux. Linux users can First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) -Open your Claude Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. +Next, open your Claude Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. For example, if you have [VS Code](https://code.visualstudio.com/) installed: @@ -378,7 +872,8 @@ Add this configuration (replace the parent folder path): -```json + +```json Python { "mcpServers": { "weather": { @@ -393,9 +888,25 @@ Add this configuration (replace the parent folder path): } } ``` + +```json Node +{ + "mcpServers": { + "weather": { + "command": "node", + "args": [ + "/PATH/TO/PARENT/FOLDER/mcp-quickstart/build/index.js", + ] + } + } +} +``` + -```json + + +```json Python { "mcpServers": { "weather": { @@ -410,24 +921,56 @@ Add this configuration (replace the parent folder path): } } ``` + +```json Node +{ + "mcpServers": { + "weather": { + "command": "node", + "args": [ + "C:\\PATH\TO\PARENT\FOLDER\mcp-quickstart\build\index.ts", + ] + } + } +} +``` + + This tells Claude Desktop: 1. There's an MCP server named "weather" -2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/mcp-quickstart run weather` +2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/mcp-quickstart run weather` if using Python or `node /PATH/TO/PARENT/FOLDER/mcp-quickstart/build/index.js` if using Node Save the file, and restart **Claude Desktop**. ### Test with commands -You can now test your server by running the following commands in Claude Desktop: - -- What's the weather in Brooklyn? -- What's the weather in San Francisco? +First, make sure Claude Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the hammer icon: - + + + +After clicking on the hammer icon, you should see two tools listed: + + + + + +If your server isn't being picked up by Claude Desktop, proceed to the **Troubleshooting** section for debugging advice. + +You can now test your server by running the following commands in Claude Desktop: + +- What's the weather in Sacramento? +- What are the active weather alerts in Texas? + + + + + + @@ -445,4 +988,79 @@ When you ask a question: 5. Claude formulates a natural language response 6. The response is displayed to you! -## Next steps \ No newline at end of file +## Troubleshooting + + + +**Error: Failed to retrieve grid point data** + +This usually means either: +1. The coordinates are outside the US +2. The NWS API is having issues +3. You're being rate limited + +Fix: + +- Verify you're using US coordinates +- Add a small delay between requests +- Check the NWS API status page + +**Error: No active alerts for [STATE]** + +This isn't an error - it just means there are no current weather alerts for that state. Try a different state or check during severe weather. + + +**Server not showing up in Claude** +1. Check your configuration file syntax +2. Make sure the path to your project is correct +3. Restart Claude Desktop completely + +You can also check Claude's logs for errors like so: +```bash +# Check Claude's logs for errors +tail -f ~/Library/Application\ Support/Claude/Claude.log +``` + +**Tool calls failing silently** + +If Claude attempts to use the tools but they fail: + +1. Check Claude's logs for errors +2. Verify your server runs without errors +3. Try restarting Claude Desktop + + + +## Next steps + + + + Learn how to build your first MCP client + + + Build an intermediate MCP server that communicates with your local filesystem + + + Check out our gallery of official MCP servers and implementations + + + + Understand how MCP connects clients, servers, and LLMs + + \ No newline at end of file