Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ The project includes Docker support for easy deployment:
- **docker-compose.yml**: Simplifies deployment with environment variables
- **.dockerignore**: Optimizes Docker builds

## MCP Client

A Python client is available to interact with the Weather MCP server programmatically.
For more details on usage and examples, please see the [MCP Client documentation in weather_mcp/README.md](./weather_mcp/README.md#mcp-client).

## Detailed Documentation

For more detailed information about the MCP server implementation, API details, and advanced usage, please refer to the [weather_mcp/README.md](weather_mcp/README.md) file.
Expand Down
78 changes: 78 additions & 0 deletions weather_mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,84 @@ The server provides a single tool: `weather.get_weather`

This implementation focuses on the core weather tool functionality. Resources have been temporarily disabled due to compatibility issues with the current version of FastMCP.

## MCP Client

This project includes a Python-based asynchronous client, `WeatherMCPClient`, designed to interact with the Weather MCP server. It simplifies making requests to the server's tools and handling responses.

### Client Installation

The client relies on the `httpx` library for asynchronous HTTP requests. To install all necessary dependencies, including those for the client, use the main `requirements.txt` file:

```bash
pip install -r weather_mcp/requirements.txt
```
Alternatively, if you only need the client and its direct dependency:
```bash
pip install httpx
```

### Basic Usage Example

Here's a basic example of how to use the `WeatherMCPClient`:

```python
import asyncio
from weather_mcp.mcp_client.weather_mcp_client import WeatherMCPClient, MCPClientError

async def main():
# Ensure the MCP server is running and accessible at this URL
client = WeatherMCPClient("http://localhost:3399/sse") # Or your server URL

try:
# Example: Get health check
print("\n--- Calling Health Check ---")
health = await client.health_check()
print("Health Check:", health)

# Example: Get weather for default city (configured on server)
print("\n--- Calling Get Weather (Default) ---")
weather_today = await client.get_weather()
print("Weather Today:", weather_today)

# Example: Get weather for London tomorrow
print("\n--- Calling Get Weather (London, 1 day) ---")
weather_london_tomorrow = await client.get_weather(city="London,uk", days=1)
print("Weather London Tomorrow:", weather_london_tomorrow)

# Example: Get the MCP info resource
print("\n--- Calling Get MCP Info Resource ---")
mcp_info = await client.get_health_resource()
print("MCP Info:", mcp_info)

except MCPClientError as e:
print(f"Client Error: {e}")
if e.status_code:
print(f"Status Code: {e.status_code}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
await client.close()

if __name__ == "__main__":
asyncio.run(main())
```

### Running the Test Script

The client module includes a test script `weather_mcp/mcp_client/test_client.py` that demonstrates the usage of all client functionalities.

**Prerequisites:**
* The Weather MCP server **must** be running and accessible (e.g., at `http://localhost:3399/sse`).
* All dependencies from `weather_mcp/requirements.txt` must be installed.

**To run the test script:**

Navigate to the root directory of the `weather_mcp` project and execute:
```bash
python weather_mcp/mcp_client/test_client.py
```
The script will call each method of the `WeatherMCPClient` (health check, get 404 page, get weather with different parameters, get MCP info resource) and print the server's responses or any errors encountered. This is a good way to verify that both the client and server are functioning correctly.

## Error Handling

The server includes comprehensive error handling for:
Expand Down
1 change: 1 addition & 0 deletions weather_mcp/mcp_client/.placeholder
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This is a placeholder file to create the directory.
1 change: 1 addition & 0 deletions weather_mcp/mcp_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file makes mcp_client a Python package.
105 changes: 105 additions & 0 deletions weather_mcp/mcp_client/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import asyncio
import sys
import os

# Adjust path to import from parent directory if necessary
# This is common when running a script directly from a subdirectory of a package
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

try:
from mcp_client.weather_mcp_client import WeatherMCPClient, MCPClientError
except ImportError:
print("Failed to import WeatherMCPClient. Ensure the client module is in the mcp_client directory and sys.path is correct.")
print(f"Current sys.path: {sys.path}")
sys.exit(1)


async def main():
# Default base URL for the MCP server
# You can override this with an environment variable if needed
mcp_base_url = os.getenv("MCP_SERVER_URL", "http://localhost:3399/sse")

print(f"Attempting to connect to MCP server at: {mcp_base_url}")
# It's good practice to ensure the URL ends with a slash for base_url in httpx
if not mcp_base_url.endswith('/'):
mcp_base_url += '/'

client = WeatherMCPClient(base_url=mcp_base_url)

try:
print("\n--- Testing health_check() ---")
try:
health_status = await client.health_check()
print("Health check successful.")
print(f"Response: {health_status}")
except MCPClientError as e:
print(f"Error during health_check: {e}")
if e.status_code:
print(f"Status code: {e.status_code}")

print("\n--- Testing get_404_page() ---")
try:
html_content = await client.get_404_page()
print("get_404_page successful.")
print(f"Response (first 100 chars): {html_content[:100]}...")
# print(f"Full HTML content length: {len(html_content)}") # Uncomment to see full length
except MCPClientError as e:
print(f"Error during get_404_page: {e}")
if e.status_code:
print(f"Status code: {e.status_code}")

print("\n--- Testing get_weather() (default) ---")
try:
weather_default = await client.get_weather()
print("get_weather (default) successful.")
print(f"Response: {weather_default}")
except MCPClientError as e:
print(f"Error during get_weather (default): {e}")
if e.status_code:
print(f"Status code: {e.status_code}")

print("\n--- Testing get_weather(city=\"London,uk\", days=1) ---")
try:
weather_london = await client.get_weather(city="London,uk", days=1)
print("get_weather (London,uk, 1 day) successful.")
print(f"Response: {weather_london}")
except MCPClientError as e:
print(f"Error during get_weather (London,uk, 1 day): {e}")
if e.status_code:
print(f"Status code: {e.status_code}")

print("\n--- Testing get_weather(city=\"NonExistentCity\", days=1) ---")
try:
weather_non_existent = await client.get_weather(city="NonExistentCityHopefully", days=1)
print("get_weather (NonExistentCityHopefully, 1 day) - this might succeed or fail depending on server handling.")
print(f"Response: {weather_non_existent}")
except MCPClientError as e:
print(f"Error during get_weather (NonExistentCityHopefully, 1 day): {e}")
if e.status_code:
print(f"Status code: {e.status_code}")

print("\n--- Testing get_health_resource() ---")
try:
health_resource = await client.get_health_resource()
print("get_health_resource successful.")
print(f"Response: {health_resource}")
except MCPClientError as e:
print(f"Error during get_health_resource: {e}")
if e.status_code:
print(f"Status code: {e.status_code}")

except MCPClientError as e:
print(f"\nA critical MCPClientError occurred: {e}")
if e.status_code:
print(f"Status code: {e.status_code}")
except Exception as e:
print(f"\nAn unexpected error occurred: {e}")
finally:
print("\n--- Closing client connection ---")
await client.close()
print("Client connection closed.")

if __name__ == "__main__":
print("Starting WeatherMCPClient test script...")
asyncio.run(main())
print("\nTest script finished.")
135 changes: 135 additions & 0 deletions weather_mcp/mcp_client/weather_mcp_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import httpx
import json
from typing import Optional, Dict, Any

class MCPClientError(Exception):
"""Custom exception for MCP client errors."""
def __init__(self, message: str, status_code: Optional[int] = None):
super().__init__(message)
self.status_code = status_code

class WeatherMCPClient:
def __init__(self, base_url: str = "http://localhost:3399/sse"):
if not base_url.endswith('/'):
base_url += '/'
self.base_url = base_url
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)

async def _request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None) -> Any:
"""Helper method to make requests to the MCP server."""
try:
response = await self._client.request(method, endpoint, params=params, json=data)
response.raise_for_status() # Raises HTTPStatusError for 4xx/5xx responses
# It seems MCP sometimes returns plain text for errors even with 200 OK for some tools
# And sometimes returns JSON, sometimes plain text for actual tool output
try:
return response.json()
except json.JSONDecodeError:
# If JSON decoding fails, return the plain text content
# This is important for tools that might return non-JSON data
# or for error messages that are not in JSON format.
text_response = response.text
# Attempt to parse known error structures if any, otherwise return raw text
# For now, we'll assume if it's not JSON, it's either a direct string result or an error string
if "error" in text_response.lower() and response.status_code >= 400: # Basic check
raise MCPClientError(f"Server returned non-JSON error: {text_response}", response.status_code)
return text_response
except httpx.HTTPStatusError as e:
# Attempt to get more detailed error from response
try:
error_details = e.response.json()
error_message = error_details.get("detail", e.response.text)
except json.JSONDecodeError:
error_message = e.response.text
raise MCPClientError(f"HTTP error occurred: {error_message}", status_code=e.response.status_code) from e
except httpx.RequestError as e:
# For network errors, timeouts, etc.
raise MCPClientError(f"Request failed: {str(e)}") from e

async def close(self):
"""Closes the HTTP client."""
await self._client.aclose()

async def health_check(self) -> Any:
"""Calls the /health_check tool on the main server."""
return await self._request("GET", "health_check")

async def get_404_page(self) -> str:
"""Calls the /get_404_page tool on the main server. Expects HTML content."""
# This tool is expected to return HTML, so we directly access .text
try:
response = await self._client.get("get_404_page")
response.raise_for_status()
return response.text
except httpx.HTTPStatusError as e:
raise MCPClientError(f"HTTP error occurred while fetching 404 page: {e.response.text}", status_code=e.response.status_code) from e
except httpx.RequestError as e:
raise MCPClientError(f"Request failed while fetching 404 page: {str(e)}") from e

async def get_weather(self, city: Optional[str] = None, days: int = 0) -> Any:
"""Calls the /weather/get_weather tool."""
params = {}
if city:
params["city"] = city
if days > 0:
params["days"] = days
return await self._request("GET", "weather/get_weather", params=params)

async def get_health_resource(self) -> Any:
"""Fetches the content of the resource at /mcp/info."""
# Resource URLs are typically relative to the base_url of the server itself, not the /sse endpoint part
# However, the problem description implies it's a "tool" call via the MCP proxy.
# Let's assume it's a standard tool call for "/mcp/info"
# If it were a direct resource fetch, it would be `self.base_url.replace('/sse', '') + "resource/mcp/info"`
# Given the context, it's more likely a tool that serves this resource.
# The original problem statement mentions "resource://mcp/info", fastmcp translates this to /mcp/info tool.
return await self._request("GET", "mcp/info")

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()

if __name__ == '__main__':
import asyncio

async def main():
# Example usage (requires a running MCP server)
# Replace with your actual MCP server URL if different
client = WeatherMCPClient(base_url="http://localhost:3399/sse")
try:
print("--- Health Check ---")
health = await client.health_check()
print(f"Health check response: {health}\n")

print("--- Get Weather (Default) ---")
weather_default = await client.get_weather()
print(f"Default weather response: {weather_default}\n")

print("--- Get Weather (London, 3 days) ---")
weather_london = await client.get_weather(city="London", days=3)
print(f"Weather for London (3 days): {weather_london}\n")

print("--- Get Weather (Paris, 1 day) ---")
weather_paris = await client.get_weather(city="Paris", days=1)
print(f"Weather for Paris (1 day): {weather_paris}\n")

print("--- Get MCP Info Resource ---")
mcp_info = await client.get_health_resource()
print(f"MCP Info response: {mcp_info}\n")

# Note: get_404_page might return a large HTML string
# print("--- Get 404 Page ---")
# page_404 = await client.get_404_page()
# print(f"404 Page content length: {len(page_404)}\n")


except MCPClientError as e:
print(f"An error occurred: {e}")
if e.status_code:
print(f"Status code: {e.status_code}")
finally:
await client.close()

asyncio.run(main())
1 change: 1 addition & 0 deletions weather_mcp/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ python-dotenv>=0.19.0
sseclient-py>=1.8.0
fastapi
uvicorn[standard]
httpx>=0.20.0 # Using a recent version, adjust as needed