summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--client.py153
-rw-r--r--main.py6
-rw-r--r--pyproject.toml2
-rw-r--r--uv.lock66
-rw-r--r--weather.py94
6 files changed, 316 insertions, 6 deletions
diff --git a/.gitignore b/.gitignore
index 505a3b1..110b0a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ wheels/
# Virtual environments
.venv
+.env
diff --git a/client.py b/client.py
new file mode 100644
index 0000000..55b3b7e
--- /dev/null
+++ b/client.py
@@ -0,0 +1,153 @@
+import asyncio
+from typing import Optional
+from contextlib import AsyncExitStack
+
+from mcp import ClientSession, StdioServerParameters
+from mcp.client.stdio import stdio_client
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv() # load environment variables from .env
+
+class MCPClient:
+ def __init__(self):
+ # Initialize session and client objects
+ self.session: Optional[ClientSession] = None
+ self.exit_stack = AsyncExitStack()
+ self.anthropic = Anthropic()
+ # methods will go here
+ async def connect_to_server(self, server_script_path: str):
+ """Connect to an MCP server
+
+ Args:
+ server_script_path: Path to the server script (.py or .js)
+ """
+ is_python = server_script_path.endswith('.py')
+ is_js = server_script_path.endswith('.js')
+ if not (is_python or is_js):
+ raise ValueError("Server script must be a .py or .js file")
+
+ command = "python" if is_python else "node"
+ server_params = StdioServerParameters(
+ command=command,
+ args=[server_script_path],
+ env=None
+ )
+
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
+ self.stdio, self.write = stdio_transport
+ self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
+
+ await self.session.initialize()
+
+ # List available tools
+ response = await self.session.list_tools()
+ tools = response.tools
+ print("\nConnected to server with tools:", [tool.name for tool in tools])
+ async def process_query(self, query: str) -> str:
+ """Process a query using Claude and available tools"""
+ messages = [
+ {
+ "role": "user",
+ "content": query
+ }
+ ]
+
+ response = await self.session.list_tools()
+ available_tools = [{
+ "name": tool.name,
+ "description": tool.description,
+ "input_schema": tool.inputSchema
+ } for tool in response.tools]
+
+ # Initial Claude API call
+ response = self.anthropic.messages.create(
+ model="claude-3-5-sonnet-20241022",
+ max_tokens=1000,
+ messages=messages,
+ tools=available_tools
+ )
+
+ # Process response and handle tool calls
+ final_text = []
+
+ assistant_message_content = []
+ for content in response.content:
+ if content.type == 'text':
+ final_text.append(content.text)
+ assistant_message_content.append(content)
+ elif content.type == 'tool_use':
+ tool_name = content.name
+ tool_args = content.input
+
+ # Execute tool call
+ result = await self.session.call_tool(tool_name, tool_args)
+ final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
+
+ assistant_message_content.append(content)
+ messages.append({
+ "role": "assistant",
+ "content": assistant_message_content
+ })
+ messages.append({
+ "role": "user",
+ "content": [
+ {
+ "type": "tool_result",
+ "tool_use_id": content.id,
+ "content": result.content
+ }
+ ]
+ })
+
+ # Get next response from Claude
+ response = self.anthropic.messages.create(
+ model="claude-3-5-sonnet-20241022",
+ max_tokens=1000,
+ messages=messages,
+ tools=available_tools
+ )
+
+ final_text.append(response.content[0].text)
+
+ return "\n".join(final_text)
+
+ async def chat_loop(self):
+ """Run an interactive chat loop"""
+ print("\nMCP Client Started!")
+ print("Type your queries or 'quit' to exit.")
+
+ while True:
+ try:
+ query = input("\nQuery: ").strip()
+
+ if query.lower() == 'quit':
+ break
+
+ response = await self.process_query(query)
+ print("\n" + response)
+
+ except Exception as e:
+ print(f"\nError: {str(e)}")
+
+ async def cleanup(self):
+ """Clean up resources"""
+ await self.exit_stack.aclose()
+
+
+async def main():
+ if len(sys.argv) < 2:
+ print("Usage: python client.py <path_to_server_script>")
+ sys.exit(1)
+
+ client = MCPClient()
+ try:
+ await client.connect_to_server(sys.argv[1])
+ await client.chat_loop()
+ finally:
+ await client.cleanup()
+
+if __name__ == "__main__":
+ import sys
+ asyncio.run(main()) \ No newline at end of file
diff --git a/main.py b/main.py
deleted file mode 100644
index c2c87a2..0000000
--- a/main.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def main():
- print("Hello from weather!")
-
-
-if __name__ == "__main__":
- main()
diff --git a/pyproject.toml b/pyproject.toml
index 85aab48..5449f85 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,6 +5,8 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
+ "anthropic>=0.49.0",
"httpx>=0.28.1",
"mcp[cli]>=1.6.0",
+ "python-dotenv>=1.1.0",
]
diff --git a/uv.lock b/uv.lock
index 046526c..8a1bd2c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -12,6 +12,24 @@ wheels = [
]
[[package]]
+name = "anthropic"
+version = "0.49.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/e3/a88c8494ce4d1a88252b9e053607e885f9b14d0a32273d47b727cbee4228/anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398", size = 210016 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368 },
+]
+
+[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
@@ -56,6 +74,15 @@ wheels = [
]
[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
+]
+
+[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
@@ -111,6 +138,41 @@ wheels = [
]
[[package]]
+name = "jiter"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/d7/c55086103d6f29b694ec79156242304adf521577530d9031317ce5338c59/jiter-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7b46249cfd6c48da28f89eb0be3f52d6fdb40ab88e2c66804f546674e539ec11", size = 309203 },
+ { url = "https://files.pythonhosted.org/packages/b0/01/f775dfee50beb420adfd6baf58d1c4d437de41c9b666ddf127c065e5a488/jiter-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:609cf3c78852f1189894383cf0b0b977665f54cb38788e3e6b941fa6d982c00e", size = 319678 },
+ { url = "https://files.pythonhosted.org/packages/ab/b8/09b73a793714726893e5d46d5c534a63709261af3d24444ad07885ce87cb/jiter-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d726a3890a54561e55a9c5faea1f7655eda7f105bd165067575ace6e65f80bb2", size = 341816 },
+ { url = "https://files.pythonhosted.org/packages/35/6f/b8f89ec5398b2b0d344257138182cc090302854ed63ed9c9051e9c673441/jiter-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e89dc075c1fef8fa9be219e249f14040270dbc507df4215c324a1839522ea75", size = 364152 },
+ { url = "https://files.pythonhosted.org/packages/9b/ca/978cc3183113b8e4484cc7e210a9ad3c6614396e7abd5407ea8aa1458eef/jiter-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e8ffa3c353b1bc4134f96f167a2082494351e42888dfcf06e944f2729cbe1d", size = 406991 },
+ { url = "https://files.pythonhosted.org/packages/13/3a/72861883e11a36d6aa314b4922125f6ae90bdccc225cd96d24cc78a66385/jiter-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:203f28a72a05ae0e129b3ed1f75f56bc419d5f91dfacd057519a8bd137b00c42", size = 395824 },
+ { url = "https://files.pythonhosted.org/packages/87/67/22728a86ef53589c3720225778f7c5fdb617080e3deaed58b04789418212/jiter-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca1a02ad60ec30bb230f65bc01f611c8608b02d269f998bc29cca8619a919dc", size = 351318 },
+ { url = "https://files.pythonhosted.org/packages/69/b9/f39728e2e2007276806d7a6609cda7fac44ffa28ca0d02c49a4f397cc0d9/jiter-0.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:237e5cee4d5d2659aaf91bbf8ec45052cc217d9446070699441a91b386ae27dc", size = 384591 },
+ { url = "https://files.pythonhosted.org/packages/eb/8f/8a708bc7fd87b8a5d861f1c118a995eccbe6d672fe10c9753e67362d0dd0/jiter-0.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:528b6b71745e7326eed73c53d4aa57e2a522242320b6f7d65b9c5af83cf49b6e", size = 520746 },
+ { url = "https://files.pythonhosted.org/packages/95/1e/65680c7488bd2365dbd2980adaf63c562d3d41d3faac192ebc7ef5b4ae25/jiter-0.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9f48e86b57bc711eb5acdfd12b6cb580a59cc9a993f6e7dcb6d8b50522dcd50d", size = 512754 },
+ { url = "https://files.pythonhosted.org/packages/78/f3/fdc43547a9ee6e93c837685da704fb6da7dba311fc022e2766d5277dfde5/jiter-0.9.0-cp312-cp312-win32.whl", hash = "sha256:699edfde481e191d81f9cf6d2211debbfe4bd92f06410e7637dffb8dd5dfde06", size = 207075 },
+ { url = "https://files.pythonhosted.org/packages/cd/9d/742b289016d155f49028fe1bfbeb935c9bf0ffeefdf77daf4a63a42bb72b/jiter-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:099500d07b43f61d8bd780466d429c45a7b25411b334c60ca875fa775f68ccb0", size = 207999 },
+ { url = "https://files.pythonhosted.org/packages/e7/1b/4cd165c362e8f2f520fdb43245e2b414f42a255921248b4f8b9c8d871ff1/jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7", size = 308197 },
+ { url = "https://files.pythonhosted.org/packages/13/aa/7a890dfe29c84c9a82064a9fe36079c7c0309c91b70c380dc138f9bea44a/jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b", size = 318160 },
+ { url = "https://files.pythonhosted.org/packages/6a/38/5888b43fc01102f733f085673c4f0be5a298f69808ec63de55051754e390/jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69", size = 341259 },
+ { url = "https://files.pythonhosted.org/packages/3d/5e/bbdbb63305bcc01006de683b6228cd061458b9b7bb9b8d9bc348a58e5dc2/jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103", size = 363730 },
+ { url = "https://files.pythonhosted.org/packages/75/85/53a3edc616992fe4af6814c25f91ee3b1e22f7678e979b6ea82d3bc0667e/jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635", size = 405126 },
+ { url = "https://files.pythonhosted.org/packages/ae/b3/1ee26b12b2693bd3f0b71d3188e4e5d817b12e3c630a09e099e0a89e28fa/jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4", size = 393668 },
+ { url = "https://files.pythonhosted.org/packages/11/87/e084ce261950c1861773ab534d49127d1517b629478304d328493f980791/jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d", size = 352350 },
+ { url = "https://files.pythonhosted.org/packages/f0/06/7dca84b04987e9df563610aa0bc154ea176e50358af532ab40ffb87434df/jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3", size = 384204 },
+ { url = "https://files.pythonhosted.org/packages/16/2f/82e1c6020db72f397dd070eec0c85ebc4df7c88967bc86d3ce9864148f28/jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5", size = 520322 },
+ { url = "https://files.pythonhosted.org/packages/36/fd/4f0cd3abe83ce208991ca61e7e5df915aa35b67f1c0633eb7cf2f2e88ec7/jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d", size = 512184 },
+ { url = "https://files.pythonhosted.org/packages/a0/3c/8a56f6d547731a0b4410a2d9d16bf39c861046f91f57c98f7cab3d2aa9ce/jiter-0.9.0-cp313-cp313-win32.whl", hash = "sha256:f7e6850991f3940f62d387ccfa54d1a92bd4bb9f89690b53aea36b4364bcab53", size = 206504 },
+ { url = "https://files.pythonhosted.org/packages/f4/1c/0c996fd90639acda75ed7fa698ee5fd7d80243057185dc2f63d4c1c9f6b9/jiter-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8ae3bf27cd1ac5e6e8b7a27487bf3ab5f82318211ec2e1346a5b058756361f7", size = 204943 },
+ { url = "https://files.pythonhosted.org/packages/78/0f/77a63ca7aa5fed9a1b9135af57e190d905bcd3702b36aca46a01090d39ad/jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001", size = 317281 },
+ { url = "https://files.pythonhosted.org/packages/f9/39/a3a1571712c2bf6ec4c657f0d66da114a63a2e32b7e4eb8e0b83295ee034/jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a", size = 350273 },
+ { url = "https://files.pythonhosted.org/packages/ee/47/3729f00f35a696e68da15d64eb9283c330e776f3b5789bac7f2c0c4df209/jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf", size = 206867 },
+]
+
+[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
@@ -354,12 +416,16 @@ name = "weather"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "anthropic" },
{ name = "httpx" },
{ name = "mcp", extra = ["cli"] },
+ { name = "python-dotenv" },
]
[package.metadata]
requires-dist = [
+ { name = "anthropic", specifier = ">=0.49.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.6.0" },
+ { name = "python-dotenv", specifier = ">=1.1.0" },
]
diff --git a/weather.py b/weather.py
index e69de29..70c0ef5 100644
--- a/weather.py
+++ b/weather.py
@@ -0,0 +1,94 @@
+from typing import Any
+import httpx
+from mcp.server.fastmcp import FastMCP
+
+# Initialize FastMCP server
+mcp = FastMCP("weather")
+
+# Constants
+NWS_API_BASE = "https://api.weather.gov"
+USER_AGENT = "weather-app/1.0"
+
+async def make_nws_request(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"
+ }
+ async with httpx.AsyncClient() as client:
+ 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 readable string."""
+ props = feature["properties"]
+ return f"""
+Event: {props.get('event', 'Unknown')}
+Area: {props.get('areaDesc', 'Unknown')}
+Severity: {props.get('severity', 'Unknown')}
+Description: {props.get('description', 'No description available')}
+Instructions: {props.get('instruction', 'No specific instructions provided')}
+"""
+
+@mcp.tool()
+async def get_alerts(state: str) -> str:
+ """Get weather alerts for a US state.
+
+ Args:
+ state: Two-letter US state code (e.g. CA, NY)
+ """
+ url = f"{NWS_API_BASE}/alerts/active/area/{state}"
+ data = await make_nws_request(url)
+
+ if not data or "features" not in data:
+ return "Unable to fetch alerts or no alerts found."
+
+ if not data["features"]:
+ return "No active alerts for this state."
+
+ alerts = [format_alert(feature) for feature in data["features"]]
+ return "\n---\n".join(alerts)
+
+@mcp.tool()
+async def get_forecast(latitude: float, longitude: float) -> str:
+ """Get weather forecast for a location.
+
+ Args:
+ latitude: Latitude of the location
+ longitude: Longitude of the location
+ """
+ # First get the forecast grid endpoint
+ points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
+ points_data = await make_nws_request(points_url)
+
+ if not points_data:
+ return "Unable to fetch forecast data for this location."
+
+ # Get the forecast URL from the points response
+ forecast_url = points_data["properties"]["forecast"]
+ forecast_data = await make_nws_request(forecast_url)
+
+ if not forecast_data:
+ return "Unable to fetch detailed forecast."
+
+ # Format the periods into a readable forecast
+ periods = forecast_data["properties"]["periods"]
+ forecasts = []
+ for period in periods[:5]: # Only show next 5 periods
+ forecast = f"""
+{period['name']}:
+Temperature: {period['temperature']}°{period['temperatureUnit']}
+Wind: {period['windSpeed']} {period['windDirection']}
+Forecast: {period['detailedForecast']}
+"""
+ forecasts.append(forecast)
+
+ return "\n---\n".join(forecasts)
+
+if __name__ == "__main__":
+ # Initialize and run the server
+ mcp.run(transport='stdio') \ No newline at end of file