Merge pull request #83 from a-akimov/mcp-client-add-ruff

Python MCP client - add ruff
This commit is contained in:
Jonathan Hefner 2025-12-29 11:24:12 -06:00 • committed by GitHub
commit d35d433645
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 98 additions and 48 deletions

View file

@ -1,34 +1,33 @@
import asyncio import asyncio
from typing import Optional
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from pathlib import Path
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic from anthropic import Anthropic
from dotenv import load_dotenv from dotenv import load_dotenv
from pathlib import Path from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
load_dotenv() # load environment variables from .env load_dotenv() # load environment variables from .env
# Claude model constant # Claude model constant
ANTHROPIC_MODEL = "claude-sonnet-4-5" ANTHROPIC_MODEL = "claude-sonnet-4-5"
class MCPClient: class MCPClient:
def __init__(self): def __init__(self):
# Initialize session and client objects # Initialize session and client objects
self.session: Optional[ClientSession] = None self.session: ClientSession | None = None
self.exit_stack = AsyncExitStack() self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic() self.anthropic = Anthropic()
async def connect_to_server(self, server_script_path: str): async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server """Connect to an MCP server
Args: Args:
server_script_path: Path to the server script (.py or .js) server_script_path: Path to the server script (.py or .js)
""" """
is_python = server_script_path.endswith('.py') is_python = server_script_path.endswith(".py")
is_js = server_script_path.endswith('.js') is_js = server_script_path.endswith(".js")
if not (is_python or is_js): if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file") raise ValueError("Server script must be a .py or .js file")
@ -40,16 +39,14 @@ class MCPClient:
env=None, env=None,
) )
else: else:
server_params = StdioServerParameters( server_params = StdioServerParameters(command="node", args=[server_script_path], env=None)
command="node", args=[server_script_path], env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize() await self.session.initialize()
# List available tools # List available tools
response = await self.session.list_tools() response = await self.session.list_tools()
tools = response.tools tools = response.tools
@ -57,52 +54,37 @@ class MCPClient:
async def process_query(self, query: str) -> str: async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools""" """Process a query using Claude and available tools"""
messages = [ messages = [{"role": "user", "content": query}]
{
"role": "user",
"content": query
}
]
response = await self.session.list_tools() response = await self.session.list_tools()
available_tools = [{ available_tools = [
"name": tool.name, {"name": tool.name, "description": tool.description, "input_schema": tool.inputSchema}
"description": tool.description, for tool in response.tools
"input_schema": tool.inputSchema ]
} for tool in response.tools]
# Initial Claude API call # Initial Claude API call
response = self.anthropic.messages.create( response = self.anthropic.messages.create(
model=ANTHROPIC_MODEL, model=ANTHROPIC_MODEL, max_tokens=1000, messages=messages, tools=available_tools
max_tokens=1000,
messages=messages,
tools=available_tools
) )
# Process response and handle tool calls # Process response and handle tool calls
final_text = [] final_text = []
for content in response.content: for content in response.content:
if content.type == 'text': if content.type == "text":
final_text.append(content.text) final_text.append(content.text)
elif content.type == 'tool_use': elif content.type == "tool_use":
tool_name = content.name tool_name = content.name
tool_args = content.input tool_args = content.input
# Execute tool call # Execute tool call
result = await self.session.call_tool(tool_name, tool_args) result = await self.session.call_tool(tool_name, tool_args)
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
# Continue conversation with tool results # Continue conversation with tool results
if hasattr(content, 'text') and content.text: if hasattr(content, "text") and content.text:
messages.append({ messages.append({"role": "assistant", "content": content.text})
"role": "assistant", messages.append({"role": "user", "content": result.content})
"content": content.text
})
messages.append({
"role": "user",
"content": result.content
})
# Get next response from Claude # Get next response from Claude
response = self.anthropic.messages.create( response = self.anthropic.messages.create(
@ -119,29 +101,30 @@ class MCPClient:
"""Run an interactive chat loop""" """Run an interactive chat loop"""
print("\nMCP Client Started!") print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.") print("Type your queries or 'quit' to exit.")
while True: while True:
try: try:
query = input("\nQuery: ").strip() query = input("\nQuery: ").strip()
if query.lower() == 'quit': if query.lower() == "quit":
break break
response = await self.process_query(query) response = await self.process_query(query)
print("\n" + response) print("\n" + response)
except Exception as e: except Exception as e:
print(f"\nError: {str(e)}") print(f"\nError: {str(e)}")
async def cleanup(self): async def cleanup(self):
"""Clean up resources""" """Clean up resources"""
await self.exit_stack.aclose() await self.exit_stack.aclose()
async def main(): async def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>") print("Usage: python client.py <path_to_server_script>")
sys.exit(1) sys.exit(1)
client = MCPClient() client = MCPClient()
try: try:
await client.connect_to_server(sys.argv[1]) await client.connect_to_server(sys.argv[1])
@ -149,6 +132,8 @@ async def main():
finally: finally:
await client.cleanup() await client.cleanup()
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
asyncio.run(main()) asyncio.run(main())

View file

@ -9,3 +9,34 @@ dependencies = [
"mcp>=1.21.0", "mcp>=1.21.0",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
] ]
[dependency-groups]
dev = [
"ruff>=0.14.9",
]
[tool.ruff]
line-length = 120
target-version = "py310"
extend-exclude = ["README.md"]
[tool.ruff.lint]
select = [
"C4", # flake8-comprehensions
"C90", # mccabe
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PERF", # Perflint
"PL", # Pylint
"UP", # pyupgrade
]
ignore = ["PERF203", "PLC0415", "PLR0402"]
mccabe.max-complexity = 24 # Default is 10
[tool.ruff.lint.pylint]
allow-magic-value-types = ["bytes", "float", "int", "str"]
max-args = 23 # Default is 5
max-branches = 23 # Default is 12
max-returns = 13 # Default is 6
max-statements = 102 # Default is 50

View file

@ -473,6 +473,11 @@ dependencies = [
{ name = "python-dotenv" }, { name = "python-dotenv" },
] ]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "anthropic", specifier = ">=0.72.0" }, { name = "anthropic", specifier = ">=0.72.0" },
@ -480,6 +485,9 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.14.9" }]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.23" version = "2.23"
@ -826,6 +834,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" }, { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" },
] ]
[[package]]
name = "ruff"
version = "0.14.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" },
{ url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" },
{ url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" },
{ url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" },
{ url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" },
{ url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" },
{ url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" },
{ url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" },
{ url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" },
{ url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" },
{ url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" },
{ url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" },
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"