diff --git a/docs/first-server/python.mdx b/docs/first-server/python.mdx deleted file mode 100644 index d48c966..0000000 --- a/docs/first-server/python.mdx +++ /dev/null @@ -1,828 +0,0 @@ ---- -title: "Python" -description: "Create a simple MCP server in Python in 15 minutes" ---- - -Let's build your first MCP server in Python! We'll create a weather server that provides current weather data as a resource and lets Claude fetch forecasts using tools. - - - This guide uses the OpenWeatherMap API. You'll need a free API key from [OpenWeatherMap](https://openweathermap.org/api) to follow along. - - -## Prerequisites - - - The following steps are for macOS. Guides for other platforms are coming soon. - - - - - You'll need Python 3.10 or higher: - - ```bash - python --version # Should be 3.10 or higher - ``` - - - - See https://docs.astral.sh/uv/ for more information. - - ```bash - brew install uv - uv --version # Should be 0.4.18 or higher - ``` - - - - ```bash - uvx create-mcp-server --path weather_service - cd weather_service - ``` - - - - ```bash - uv add httpx python-dotenv - ``` - - - - Create `.env`: - - ```bash - OPENWEATHER_API_KEY=your-api-key-here - ``` - - - -## Create your server - - - - In `weather_service/src/weather_service/server.py` - ```python - import os - import json - import logging - from datetime import datetime, timedelta - from collections.abc import Sequence - from functools import lru_cache - from typing import Any - - import httpx - import asyncio - from dotenv import load_dotenv - from mcp.server import Server - from mcp.types import ( - Resource, - Tool, - TextContent, - ImageContent, - EmbeddedResource, - LoggingLevel - ) - from pydantic import AnyUrl - - # Load environment variables - load_dotenv() - - # Configure logging - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger("weather-server") - - # API configuration - API_KEY = os.getenv("OPENWEATHER_API_KEY") - if not API_KEY: - raise ValueError("OPENWEATHER_API_KEY environment variable required") - - API_BASE_URL = "http://api.openweathermap.org/data/2.5" - DEFAULT_CITY = "London" - CURRENT_WEATHER_ENDPOINT = "weather" - FORECAST_ENDPOINT = "forecast" - - # The rest of our server implementation will go here - ``` - - - - Add this functionality: - - ```python - # Create reusable params - http_params = { - "appid": API_KEY, - "units": "metric" - } - - async def fetch_weather(city: str) -> dict[str, Any]: - async with httpx.AsyncClient() as client: - response = await client.get( - f"{API_BASE_URL}/weather", - params={"q": city, **http_params} - ) - response.raise_for_status() - data = response.json() - - return { - "temperature": data["main"]["temp"], - "conditions": data["weather"][0]["description"], - "humidity": data["main"]["humidity"], - "wind_speed": data["wind"]["speed"], - "timestamp": datetime.now().isoformat() - } - - - app = Server("weather-server") - ``` - - - - Add these resource-related handlers to our main function: - - ```python - app = Server("weather-server") - - @app.list_resources() - async def list_resources() -> list[Resource]: - """List available weather resources.""" - uri = AnyUrl(f"weather://{DEFAULT_CITY}/current") - return [ - Resource( - uri=uri, - name=f"Current weather in {DEFAULT_CITY}", - mimeType="application/json", - description="Real-time weather data" - ) - ] - - @app.read_resource() - async def read_resource(uri: AnyUrl) -> str: - """Read current weather data for a city.""" - city = DEFAULT_CITY - if str(uri).startswith("weather://") and str(uri).endswith("/current"): - city = str(uri).split("/")[-2] - else: - raise ValueError(f"Unknown resource: {uri}") - - try: - weather_data = await fetch_weather(city) - return json.dumps(weather_data, indent=2) - except httpx.HTTPError as e: - raise RuntimeError(f"Weather API error: {str(e)}") - - ``` - - - - Add these tool-related handlers: - - ```python - app = Server("weather-server") - - # Resource implementation ... - - @app.list_tools() - async def list_tools() -> list[Tool]: - """List available weather tools.""" - return [ - Tool( - name="get_forecast", - description="Get weather forecast for a city", - inputSchema={ - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "City name" - }, - "days": { - "type": "number", - "description": "Number of days (1-5)", - "minimum": 1, - "maximum": 5 - } - }, - "required": ["city"] - } - ) - ] - - @app.call_tool() - async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]: - """Handle tool calls for weather forecasts.""" - if name != "get_forecast": - raise ValueError(f"Unknown tool: {name}") - - if not isinstance(arguments, dict) or "city" not in arguments: - raise ValueError("Invalid forecast arguments") - - city = arguments["city"] - days = min(int(arguments.get("days", 3)), 5) - - try: - async with httpx.AsyncClient() as client: - response = await client.get( - f"{API_BASE_URL}/{FORECAST_ENDPOINT}", - params={ - "q": city, - "cnt": days * 8, # API returns 3-hour intervals - **http_params, - } - ) - response.raise_for_status() - data = response.json() - - forecasts = [] - for i in range(0, len(data["list"]), 8): - day_data = data["list"][i] - forecasts.append({ - "date": day_data["dt_txt"].split()[0], - "temperature": day_data["main"]["temp"], - "conditions": day_data["weather"][0]["description"] - }) - - return [ - TextContent( - type="text", - text=json.dumps(forecasts, indent=2) - ) - ] - except httpx.HTTPError as e: - logger.error(f"Weather API error: {str(e)}") - raise RuntimeError(f"Weather API error: {str(e)}") - ``` - - - - Add this to the end of `weather_service/src/weather_service/server.py`: - - ```python - async def main(): - # Import here to avoid issues with event loops - from mcp.server.stdio import stdio_server - - async with stdio_server() as (read_stream, write_stream): - await app.run( - read_stream, - write_stream, - app.create_initialization_options() - ) - ``` - - - - Add this to the end of `weather_service/src/weather_service/__init__.py`: - - ```python - from . import server - import asyncio - - def main(): - """Main entry point for the package.""" - asyncio.run(server.main()) - - # Optionally expose other important items at package level - __all__ = ['main', 'server'] - ``` - - - - - - -## Connect to Claude Desktop - - - - Add to `claude_desktop_config.json`: - - ```json - { - "mcpServers": { - "weather": { - "command": "uv", - "args": [ - "--directory", - "path/to/your/project", - "run", - "weather-service" - ], - "env": { - "OPENWEATHER_API_KEY": "your-api-key" - } - } - } - } - ``` - - - - 1. Quit Claude completely - - 2. Start Claude again - - 3. Look for your weather server in the 🔌 menu - - - -## Try it out! - - - - Ask Claude: - - ``` - What's the current weather in San Francisco? Can you analyze the conditions and tell me if it's a good day for outdoor activities? - ``` - - - - Ask Claude: - - ``` - Can you get me a 5-day forecast for Tokyo and help me plan what clothes to pack for my trip? - ``` - - - - Ask Claude: - - ``` - Can you analyze the forecast for both Tokyo and San Francisco and tell me which city would be better for outdoor photography this week? - ``` - - - -## Understanding the code - - - - ```python - async def read_resource(self, uri: str) -> ReadResourceResult: - # ... - ``` - - Python type hints help catch errors early and improve code maintainability. - - - - ```python - @app.list_resources() - async def list_resources(self) -> ListResourcesResult: - return ListResourcesResult( - resources=[ - Resource( - uri=f"weather://{DEFAULT_CITY}/current", - name=f"Current weather in {DEFAULT_CITY}", - mimeType="application/json", - description="Real-time weather data" - ) - ] - ) - ``` - - Resources provide data that Claude can access as context. - - - - ```python - Tool( - name="get_forecast", - description="Get weather forecast for a city", - inputSchema={ - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "City name" - }, - "days": { - "type": "number", - "description": "Number of days (1-5)", - "minimum": 1, - "maximum": 5 - } - }, - "required": ["city"] - } - ) - ``` - - Tools let Claude take actions through your server with validated inputs. - - - - ```python - # Create server instance with name - app = Server("weather-server") - - # Register resource handler - @app.list_resources() - async def list_resources() -> list[Resource]: - """List available resources""" - return [...] - - # Register tool handler - @app.call_tool() - async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]: - """Handle tool execution""" - return [...] - - # Register additional handlers - @app.read_resource() - ... - @app.list_tools() - ... - ``` - - The MCP server uses a simple app pattern - create a Server instance and register handlers with decorators. Each handler maps to a specific MCP protocol operation. - - - -## Best practices - - - - ```python - try: - async with httpx.AsyncClient() as client: - response = await client.get(..., params={..., **http_params}) - response.raise_for_status() - except httpx.HTTPError as e: - raise McpError( - ErrorCode.INTERNAL_ERROR, - f"API error: {str(e)}" - ) - ``` - - - - ```python - if not isinstance(args, dict) or "city" not in args: - raise McpError( - ErrorCode.INVALID_PARAMS, - "Invalid forecast arguments" - ) - ``` - - - - ```python - if not API_KEY: - raise ValueError("OPENWEATHER_API_KEY is required") - ``` - - - - -## Available transports - -While this guide uses stdio transport, MCP supports additional transport options: - -### SSE (Server-Sent Events) - -```python -from mcp.server.sse import SseServerTransport -from starlette.applications import Starlette -from starlette.routing import Route - -# Create SSE transport with endpoint -sse = SseServerTransport("/messages") - -# Handler for SSE connections -async def handle_sse(scope, receive, send): - async with sse.connect_sse(scope, receive, send) as streams: - await app.run( - streams[0], streams[1], app.create_initialization_options() - ) - -# Handler for client messages -async def handle_messages(scope, receive, send): - await sse.handle_post_message(scope, receive, send) - -# Create Starlette app with routes -app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse), - Route("/messages", endpoint=handle_messages, methods=["POST"]), - ], -) - -# Run with any ASGI server -import uvicorn -uvicorn.run(app, host="0.0.0.0", port=8000) -``` - -## Advanced features - - - - The request context provides access to the current request's metadata and the active client session. Access it through `server.request_context`: - - ```python - @app.call_tool() - async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]: - # Access the current request context - ctx = self.request_context - - # Get request metadata like progress tokens - if progress_token := ctx.meta.progressToken: - # Send progress notifications via the session - await ctx.session.send_progress_notification( - progress_token=progress_token, - progress=0.5, - total=1.0 - ) - - # Sample from the LLM client - result = await ctx.session.create_message( - messages=[ - SamplingMessage( - role="user", - content=TextContent( - type="text", - text="Analyze this weather data: " + json.dumps(arguments) - ) - ) - ], - max_tokens=100 - ) - - return [TextContent(type="text", text=result.content.text)] - ``` - - - - ```python - # Cache settings - cache_timeout = timedelta(minutes=15) - last_cache_time = None - cached_weather = None - - async def fetch_weather(city: str) -> dict[str, Any]: - global cached_weather, last_cache_time - - now = datetime.now() - if (cached_weather is None or - last_cache_time is None or - now - last_cache_time > cache_timeout): - - async with httpx.AsyncClient() as client: - response = await client.get( - f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}", - params={"q": city, **http_params} - ) - response.raise_for_status() - data = response.json() - - cached_weather = { - "temperature": data["main"]["temp"], - "conditions": data["weather"][0]["description"], - "humidity": data["main"]["humidity"], - "wind_speed": data["wind"]["speed"], - "timestamp": datetime.now().isoformat() - } - last_cache_time = now - - return cached_weather - ``` - - - - ```python - @self.call_tool() - async def call_tool(self, name: str, arguments: Any) -> CallToolResult: - if progress_token := self.request_context.meta.progressToken: - # Send progress notifications - await self.request_context.session.send_progress_notification( - progress_token=progress_token, - progress=1, - total=2 - ) - - # Fetch data... - - await self.request_context.session.send_progress_notification( - progress_token=progress_token, - progress=2, - total=2 - ) - - # Rest of the method implementation... - ``` - - - - ```python - # Set up logging - logger = logging.getLogger("weather-server") - logger.setLevel(logging.INFO) - - @app.set_logging_level() - async def set_logging_level(level: LoggingLevel) -> EmptyResult: - logger.setLevel(level.upper()) - await app.request_context.session.send_log_message( - level="info", - data=f"Log level set to {level}", - logger="weather-server" - ) - return EmptyResult() - - # Use logger throughout the code - # For example: - # logger.info("Weather data fetched successfully") - # logger.error(f"Error fetching weather data: {str(e)}") - ``` - - - - ```python - @app.list_resource_templates() - async def list_resource_templates() -> list[ResourceTemplate]: - return [ - ResourceTemplate( - uriTemplate="weather://{city}/current", - name="Current weather for any city", - mimeType="application/json" - ) - ] - ``` - - - -## Testing - - - - Create `tests/weather_test.py`: - - ```python - import pytest - import os - from unittest.mock import patch, Mock - from datetime import datetime - import json - from pydantic import AnyUrl - os.environ["OPENWEATHER_API_KEY"] = "TEST" - - from weather_service.server import ( - fetch_weather, - read_resource, - call_tool, - list_resources, - list_tools, - DEFAULT_CITY - ) - - @pytest.fixture - def anyio_backend(): - return "asyncio" - - @pytest.fixture - def mock_weather_response(): - return { - "main": { - "temp": 20.5, - "humidity": 65 - }, - "weather": [ - {"description": "scattered clouds"} - ], - "wind": { - "speed": 3.6 - } - } - - @pytest.fixture - def mock_forecast_response(): - return { - "list": [ - { - "dt_txt": "2024-01-01 12:00:00", - "main": {"temp": 18.5}, - "weather": [{"description": "sunny"}] - }, - { - "dt_txt": "2024-01-02 12:00:00", - "main": {"temp": 17.2}, - "weather": [{"description": "cloudy"}] - } - ] - } - - @pytest.mark.anyio - async def test_fetch_weather(mock_weather_response): - with patch('requests.Session.get') as mock_get: - mock_get.return_value.json.return_value = mock_weather_response - mock_get.return_value.raise_for_status = Mock() - - weather = await fetch_weather("London") - - assert weather["temperature"] == 20.5 - assert weather["conditions"] == "scattered clouds" - assert weather["humidity"] == 65 - assert weather["wind_speed"] == 3.6 - assert "timestamp" in weather - - @pytest.mark.anyio - async def test_read_resource(): - with patch('weather_service.server.fetch_weather') as mock_fetch: - mock_fetch.return_value = { - "temperature": 20.5, - "conditions": "clear sky", - "timestamp": datetime.now().isoformat() - } - - uri = AnyUrl("weather://London/current") - result = await read_resource(uri) - - assert isinstance(result, str) - assert "temperature" in result - assert "clear sky" in result - - @pytest.mark.anyio - async def test_call_tool(mock_forecast_response): - class Response(): - def raise_for_status(self): - pass - - def json(self): - return mock_forecast_response - - class AsyncClient(): - async def __aenter__(self): - return self - - async def __aexit__(self, *exc_info): - pass - - async def get(self, *args, **kwargs): - return Response() - - with patch('httpx.AsyncClient', new=AsyncClient) as mock_client: - result = await call_tool("get_forecast", {"city": "London", "days": 2}) - - assert len(result) == 1 - assert result[0].type == "text" - forecast_data = json.loads(result[0].text) - assert len(forecast_data) == 1 - assert forecast_data[0]["temperature"] == 18.5 - assert forecast_data[0]["conditions"] == "sunny" - - @pytest.mark.anyio - async def test_list_resources(): - resources = await list_resources() - assert len(resources) == 1 - assert resources[0].name == f"Current weather in {DEFAULT_CITY}" - assert resources[0].mimeType == "application/json" - - @pytest.mark.anyio - async def test_list_tools(): - tools = await list_tools() - assert len(tools) == 1 - assert tools[0].name == "get_forecast" - assert "city" in tools[0].inputSchema["properties"] - ``` - - - ```bash - uv add --dev pytest - uv run pytest - ``` - - - -## Troubleshooting - -### Installation issues - -```bash -# Check Python version -python --version - -# Reinstall dependencies -uv sync --reinstall -``` - -### Type checking - -```bash -# Install mypy -uv add --dev pyright - -# Run type checker -uv run pyright src -``` - -## Next steps - - - - Learn more about the MCP architecture - - - - Check out the Python SDK on GitHub - - diff --git a/docs/first-server/typescript.mdx b/docs/first-server/typescript.mdx deleted file mode 100644 index 0f39531..0000000 --- a/docs/first-server/typescript.mdx +++ /dev/null @@ -1,599 +0,0 @@ ---- -title: "TypeScript" -description: "Create a simple MCP server in TypeScript in 15 minutes" ---- - -Let's build your first MCP server in TypeScript! We'll create a weather server that provides current weather data as a resource and lets Claude fetch forecasts using tools. - - -This guide uses the OpenWeatherMap API. You'll need a free API key from [OpenWeatherMap](https://openweathermap.org/api) to follow along. - - -## Prerequisites - - - - You'll need Node.js 18 or higher: - - ```bash - node --version # Should be v18 or higher - npm --version - ``` - - - - You can use our [create-typescript-server](https://github.com/modelcontextprotocol/create-typescript-server) tool to bootstrap a new project: - - ```bash - npx @modelcontextprotocol/create-server weather-server - cd weather-server - ``` - - - - ```bash - npm install --save axios dotenv - ``` - - - - Create `.env`: - ```bash - OPENWEATHER_API_KEY=your-api-key-here - ``` - Make sure to add your environment file to `.gitignore` - ```bash - .env - ``` - - - -## Create your server - - - - Create a file `src/types.ts`, and add the following: - - ```typescript - export interface OpenWeatherResponse { - main: { - temp: number; - humidity: number; - }; - weather: Array<{ - description: string; - }>; - wind: { - speed: number; - }; - dt_txt?: string; - } - - export interface WeatherData { - temperature: number; - conditions: string; - humidity: number; - wind_speed: number; - timestamp: string; - } - - export interface ForecastDay { - date: string; - temperature: number; - conditions: string; - } - - export interface GetForecastArgs { - city: string; - days?: number; - } - - // Type guard for forecast arguments - export function isValidForecastArgs(args: any): args is GetForecastArgs { - return ( - typeof args === "object" && - args !== null && - "city" in args && - typeof args.city === "string" && - (args.days === undefined || typeof args.days === "number") - ); - } - ``` - - - - Replace `src/index.ts` with the following: - - ```typescript - #!/usr/bin/env node - import { Server } from "@modelcontextprotocol/sdk/server/index.js"; - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; - import { - ListResourcesRequestSchema, - ReadResourceRequestSchema, - ListToolsRequestSchema, - CallToolRequestSchema, - ErrorCode, - McpError - } from "@modelcontextprotocol/sdk/types.js"; - import axios from "axios"; - import dotenv from "dotenv"; - import { - WeatherData, - ForecastDay, - OpenWeatherResponse, - isValidForecastArgs - } from "./types.js"; - - dotenv.config(); - - const API_KEY = process.env.OPENWEATHER_API_KEY; - if (!API_KEY) { - throw new Error("OPENWEATHER_API_KEY environment variable is required"); - } - - const API_CONFIG = { - BASE_URL: 'http://api.openweathermap.org/data/2.5', - DEFAULT_CITY: 'San Francisco', - ENDPOINTS: { - CURRENT: 'weather', - FORECAST: 'forecast' - } - } as const; - - class WeatherServer { - private server: Server; - private axiosInstance; - - constructor() { - this.server = new Server({ - name: "example-weather-server", - version: "0.1.0" - }, { - capabilities: { - resources: {}, - tools: {} - } - }); - - // Configure axios with defaults - this.axiosInstance = axios.create({ - baseURL: API_CONFIG.BASE_URL, - params: { - appid: API_KEY, - units: "metric" - } - }); - - this.setupHandlers(); - this.setupErrorHandling(); - } - - private setupErrorHandling(): void { - this.server.onerror = (error) => { - console.error("[MCP Error]", error); - }; - - process.on('SIGINT', async () => { - await this.server.close(); - process.exit(0); - }); - } - - private setupHandlers(): void { - this.setupResourceHandlers(); - this.setupToolHandlers(); - } - - private setupResourceHandlers(): void { - // Implementation continues in next section - } - - private setupToolHandlers(): void { - // Implementation continues in next section - } - - async run(): Promise { - const transport = new StdioServerTransport(); - await this.server.connect(transport); - - // Although this is just an informative message, we must log to stderr, - // to avoid interfering with MCP communication that happens on stdout - console.error("Weather MCP server running on stdio"); - } - } - - const server = new WeatherServer(); - server.run().catch(console.error); - ``` - - - - Add this to the `setupResourceHandlers` method: - - ```typescript - private setupResourceHandlers(): void { - this.server.setRequestHandler( - ListResourcesRequestSchema, - async () => ({ - resources: [{ - uri: `weather://${API_CONFIG.DEFAULT_CITY}/current`, - name: `Current weather in ${API_CONFIG.DEFAULT_CITY}`, - mimeType: "application/json", - description: "Real-time weather data including temperature, conditions, humidity, and wind speed" - }] - }) - ); - - this.server.setRequestHandler( - ReadResourceRequestSchema, - async (request) => { - const city = API_CONFIG.DEFAULT_CITY; - if (request.params.uri !== `weather://${city}/current`) { - throw new McpError( - ErrorCode.InvalidRequest, - `Unknown resource: ${request.params.uri}` - ); - } - - try { - const response = await this.axiosInstance.get( - API_CONFIG.ENDPOINTS.CURRENT, - { - params: { q: city } - } - ); - - const weatherData: WeatherData = { - temperature: response.data.main.temp, - conditions: response.data.weather[0].description, - humidity: response.data.main.humidity, - wind_speed: response.data.wind.speed, - timestamp: new Date().toISOString() - }; - - return { - contents: [{ - uri: request.params.uri, - mimeType: "application/json", - text: JSON.stringify(weatherData, null, 2) - }] - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new McpError( - ErrorCode.InternalError, - `Weather API error: ${error.response?.data.message ?? error.message}` - ); - } - throw error; - } - } - ); - } - ``` - - - - Add these handlers to the `setupToolHandlers` method: - - ```typescript - private setupToolHandlers(): void { - this.server.setRequestHandler( - ListToolsRequestSchema, - async () => ({ - tools: [{ - name: "get_forecast", - description: "Get weather forecast for a city", - inputSchema: { - type: "object", - properties: { - city: { - type: "string", - description: "City name" - }, - days: { - type: "number", - description: "Number of days (1-5)", - minimum: 1, - maximum: 5 - } - }, - required: ["city"] - } - }] - }) - ); - - this.server.setRequestHandler( - CallToolRequestSchema, - async (request) => { - if (request.params.name !== "get_forecast") { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown tool: ${request.params.name}` - ); - } - - if (!isValidForecastArgs(request.params.arguments)) { - throw new McpError( - ErrorCode.InvalidParams, - "Invalid forecast arguments" - ); - } - - const city = request.params.arguments.city; - const days = Math.min(request.params.arguments.days || 3, 5); - - try { - const response = await this.axiosInstance.get<{ - list: OpenWeatherResponse[] - }>(API_CONFIG.ENDPOINTS.FORECAST, { - params: { - q: city, - cnt: days * 8 // API returns 3-hour intervals - } - }); - - const forecasts: ForecastDay[] = []; - for (let i = 0; i < response.data.list.length; i += 8) { - const dayData = response.data.list[i]; - forecasts.push({ - date: dayData.dt_txt?.split(' ')[0] ?? new Date().toISOString().split('T')[0], - temperature: dayData.main.temp, - conditions: dayData.weather[0].description - }); - } - - return { - content: [{ - type: "text", - text: JSON.stringify(forecasts, null, 2) - }] - }; - } catch (error) { - if (axios.isAxiosError(error)) { - return { - content: [{ - type: "text", - text: `Weather API error: ${error.response?.data.message ?? error.message}` - }], - isError: true, - } - } - throw error; - } - } - ); - } - ``` - - - - ```bash - npm run build - ``` - - - -## Connect to Claude Desktop - - - - If you didn't already connect to Claude Desktop during project setup, add to `claude_desktop_config.json`: - - ```json - { - "mcpServers": { - "weather": { - "command": "node", - "args": ["/path/to/weather-server/build/index.js"], - "env": { - "OPENWEATHER_API_KEY": "your-api-key", - } - } - } - } - ``` - - - - 1. Quit Claude completely - 2. Start Claude again - 3. Look for your weather server in the 🔌 menu - - - -## Try it out! - - - - Ask Claude: - ``` - What's the current weather in San Francisco? Can you analyze the conditions? - ``` - - - - Ask Claude: - ``` - Can you get me a 5-day forecast for Tokyo and tell me if I should pack an umbrella? - ``` - - - - Ask Claude: - ``` - Can you analyze the forecast for both Tokyo and San Francisco and tell me which city will be warmer this week? - ``` - - - -## Understanding the code - - - - ```typescript - interface WeatherData { - temperature: number; - conditions: string; - humidity: number; - wind_speed: number; - timestamp: string; - } - ``` - TypeScript adds type safety to our MCP server, making it more reliable and easier to maintain. - - - - ```typescript - this.server.setRequestHandler( - ListResourcesRequestSchema, - async () => ({ - resources: [{ - uri: `weather://${DEFAULT_CITY}/current`, - name: `Current weather in ${DEFAULT_CITY}`, - mimeType: "application/json" - }] - }) - ); - ``` - Resources provide data that Claude can access as context. - - - - ```typescript - { - name: "get_forecast", - description: "Get weather forecast for a city", - inputSchema: { - type: "object", - properties: { - city: { type: "string" }, - days: { type: "number" } - } - } - } - ``` - Tools let Claude take actions through your server with type-safe inputs. - - - -## Best practices - - - - When a tool encounters an error, return the error message with `isError: true`, so the model can self-correct: - - ```typescript - try { - const response = await axiosInstance.get(...); - } catch (error) { - if (axios.isAxiosError(error)) { - return { - content: { - mimeType: "text/plain", - text: `Weather API error: ${error.response?.data.message ?? error.message}` - }, - isError: true, - } - } - throw error; - } - ``` - - For other handlers, throw an error, so the application can notify the user: - - ```typescript - try { - const response = await this.axiosInstance.get(...); - } catch (error) { - if (axios.isAxiosError(error)) { - throw new McpError( - ErrorCode.InternalError, - `Weather API error: ${error.response?.data.message}` - ); - } - throw error; - } - ``` - - - - ```typescript - function isValidForecastArgs(args: any): args is GetForecastArgs { - return ( - typeof args === "object" && - args !== null && - "city" in args && - typeof args.city === "string" - ); - } - ``` - - You can also use libraries like [Zod](https://zod.dev/) to perform this validation automatically. - - - -## Available transports - -While this guide uses stdio to run the MCP server as a local process, MCP supports other [transports](/docs/concepts/transports) as well. - -## Troubleshooting - - - The following troubleshooting tips are for macOS. Guides for other platforms are coming soon. - - -### Build errors -```bash -# Check TypeScript version -npx tsc --version - -# Clean and rebuild -rm -rf build/ -npm run build -``` - -### Runtime errors -Look for detailed error messages in the Claude Desktop logs: -```bash -# Monitor logs -tail -n 20 -f ~/Library/Logs/Claude/mcp*.log -``` - -### Type errors -```bash -# Check types without building -npx tsc --noEmit -``` - -## Next steps - - - - Learn more about the MCP architecture - - - - Check out the TypeScript SDK on GitHub - - - - -Need help? Ask Claude! Since it has access to the MCP SDK documentation, it can help you debug issues and suggest improvements to your server. -