Add type checker to examples/client (#1837)
This commit is contained in:
committed by
GitHub
parent
3863f203e9
commit
1fd557afdc
@@ -29,7 +29,8 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import ParseResult, parse_qs, urlparse
|
||||
from typing import Any, cast
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
from mcp import ClientSession
|
||||
@@ -39,12 +40,12 @@ from mcp.client.auth.extensions.client_credentials import (
|
||||
PrivateKeyJWTOAuthProvider,
|
||||
SignedJWTParameters,
|
||||
)
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
|
||||
from pydantic import AnyUrl
|
||||
|
||||
|
||||
def get_conformance_context() -> dict:
|
||||
def get_conformance_context() -> dict[str, Any]:
|
||||
"""Load conformance test context from MCP_CONFORMANCE_CONTEXT environment variable."""
|
||||
context_json = os.environ.get("MCP_CONFORMANCE_CONTEXT")
|
||||
if not context_json:
|
||||
@@ -116,9 +117,9 @@ class ConformanceOAuthCallbackHandler:
|
||||
|
||||
# Check for redirect response
|
||||
if response.status_code in (301, 302, 303, 307, 308):
|
||||
location = response.headers.get("location")
|
||||
location = cast(str, response.headers.get("location"))
|
||||
if location:
|
||||
redirect_url: ParseResult = urlparse(location)
|
||||
redirect_url = urlparse(location)
|
||||
query_params: dict[str, list[str]] = parse_qs(redirect_url.query)
|
||||
|
||||
if "code" in query_params:
|
||||
@@ -259,12 +260,8 @@ async def run_client_credentials_basic_client(server_url: str) -> None:
|
||||
async def _run_session(server_url: str, oauth_auth: OAuthClientProvider) -> None:
|
||||
"""Common session logic for all OAuth flows."""
|
||||
# Connect using streamable HTTP transport with OAuth
|
||||
async with streamablehttp_client(
|
||||
url=server_url,
|
||||
auth=oauth_auth,
|
||||
timeout=30.0,
|
||||
sse_read_timeout=60.0,
|
||||
) as (read_stream, write_stream, _):
|
||||
client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)
|
||||
async with streamable_http_client(url=server_url, http_client=client) as (read_stream, write_stream, _):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
# Initialize the session
|
||||
await session.initialize()
|
||||
|
||||
@@ -6,21 +6,26 @@ This client connects to an MCP server using streamable HTTP transport with OAuth
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import socketserver
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
|
||||
from mcp.shared.message import SessionMessage
|
||||
|
||||
|
||||
class InMemoryTokenStorage(TokenStorage):
|
||||
@@ -46,7 +51,13 @@ class InMemoryTokenStorage(TokenStorage):
|
||||
class CallbackHandler(BaseHTTPRequestHandler):
|
||||
"""Simple HTTP handler to capture OAuth callback."""
|
||||
|
||||
def __init__(self, request, client_address, server, callback_data):
|
||||
def __init__(
|
||||
self,
|
||||
request: Any,
|
||||
client_address: tuple[str, int],
|
||||
server: socketserver.BaseServer,
|
||||
callback_data: dict[str, Any],
|
||||
):
|
||||
"""Initialize with callback data storage."""
|
||||
self.callback_data = callback_data
|
||||
super().__init__(request, client_address, server)
|
||||
@@ -91,15 +102,14 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
def log_message(self, format: str, *args: Any):
|
||||
"""Suppress default logging."""
|
||||
pass
|
||||
|
||||
|
||||
class CallbackServer:
|
||||
"""Simple server to handle OAuth callbacks."""
|
||||
|
||||
def __init__(self, port=3000):
|
||||
def __init__(self, port: int = 3000):
|
||||
self.port = port
|
||||
self.server = None
|
||||
self.thread = None
|
||||
@@ -110,7 +120,12 @@ class CallbackServer:
|
||||
callback_data = self.callback_data
|
||||
|
||||
class DataCallbackHandler(CallbackHandler):
|
||||
def __init__(self, request, client_address, server):
|
||||
def __init__(
|
||||
self,
|
||||
request: BaseHTTPRequestHandler,
|
||||
client_address: tuple[str, int],
|
||||
server: socketserver.BaseServer,
|
||||
):
|
||||
super().__init__(request, client_address, server, callback_data)
|
||||
|
||||
return DataCallbackHandler
|
||||
@@ -131,7 +146,7 @@ class CallbackServer:
|
||||
if self.thread:
|
||||
self.thread.join(timeout=1)
|
||||
|
||||
def wait_for_callback(self, timeout=300):
|
||||
def wait_for_callback(self, timeout: int = 300):
|
||||
"""Wait for OAuth callback with timeout."""
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
@@ -225,7 +240,12 @@ class SimpleAuthClient:
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
async def _run_session(self, read_stream, write_stream, get_session_id):
|
||||
async def _run_session(
|
||||
self,
|
||||
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
|
||||
write_stream: MemoryObjectSendStream[SessionMessage],
|
||||
get_session_id: Callable[[], str | None] | None = None,
|
||||
):
|
||||
"""Run the MCP session with the given streams."""
|
||||
print("🤝 Initializing MCP session...")
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
@@ -314,7 +334,7 @@ class SimpleAuthClient:
|
||||
continue
|
||||
|
||||
# Parse arguments (simple JSON-like format)
|
||||
arguments = {}
|
||||
arguments: dict[str, Any] = {}
|
||||
if len(parts) > 2:
|
||||
import json
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
@@ -93,7 +95,7 @@ class Server:
|
||||
await self.cleanup()
|
||||
raise
|
||||
|
||||
async def list_tools(self) -> list[Any]:
|
||||
async def list_tools(self) -> list[Tool]:
|
||||
"""List available tools from the server.
|
||||
|
||||
Returns:
|
||||
@@ -106,10 +108,10 @@ class Server:
|
||||
raise RuntimeError(f"Server {self.name} not initialized")
|
||||
|
||||
tools_response = await self.session.list_tools()
|
||||
tools = []
|
||||
tools: list[Tool] = []
|
||||
|
||||
for item in tools_response:
|
||||
if isinstance(item, tuple) and item[0] == "tools":
|
||||
if item[0] == "tools":
|
||||
tools.extend(Tool(tool.name, tool.description, tool.inputSchema, tool.title) for tool in item[1])
|
||||
|
||||
return tools
|
||||
@@ -189,7 +191,7 @@ class Tool:
|
||||
Returns:
|
||||
A formatted string describing the tool.
|
||||
"""
|
||||
args_desc = []
|
||||
args_desc: list[str] = []
|
||||
if "properties" in self.input_schema:
|
||||
for param_name, param_info in self.input_schema["properties"].items():
|
||||
arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}"
|
||||
@@ -311,9 +313,9 @@ class ChatSession:
|
||||
result = await server.execute_tool(tool_call["tool"], tool_call["arguments"])
|
||||
|
||||
if isinstance(result, dict) and "progress" in result:
|
||||
progress = result["progress"]
|
||||
total = result["total"]
|
||||
percentage = (progress / total) * 100
|
||||
progress = result["progress"] # type: ignore
|
||||
total = result["total"] # type: ignore
|
||||
percentage = (progress / total) * 100 # type: ignore
|
||||
logging.info(f"Progress: {progress}/{total} ({percentage:.1f}%)")
|
||||
|
||||
return f"Tool execution result: {result}"
|
||||
@@ -338,7 +340,7 @@ class ChatSession:
|
||||
await self.cleanup_servers()
|
||||
return
|
||||
|
||||
all_tools = []
|
||||
all_tools: list[Tool] = []
|
||||
for server in self.servers:
|
||||
tools = await server.list_tools()
|
||||
all_tools.extend(tools)
|
||||
|
||||
@@ -4,12 +4,12 @@ import asyncio
|
||||
|
||||
import click
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
|
||||
async def run(url: str) -> None:
|
||||
async with streamablehttp_client(url) as (read, write, _):
|
||||
async with streamable_http_client(url) as (read, write, _):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
|
||||
@@ -28,12 +28,13 @@ async def run(url: str) -> None:
|
||||
task_id = result.task.taskId
|
||||
print(f"Task created: {task_id}")
|
||||
|
||||
status = None
|
||||
# Poll until done (respects server's pollInterval hint)
|
||||
async for status in session.experimental.poll_task(task_id):
|
||||
print(f" Status: {status.status} - {status.statusMessage or ''}")
|
||||
|
||||
# Check final status
|
||||
if status.status != "completed":
|
||||
if status and status.status != "completed":
|
||||
print(f"Task ended with status: {status.status}")
|
||||
return
|
||||
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ from typing import Any
|
||||
|
||||
import click
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.types import (
|
||||
CallToolResult,
|
||||
@@ -73,7 +73,7 @@ def get_text(result: CallToolResult) -> str:
|
||||
|
||||
|
||||
async def run(url: str) -> None:
|
||||
async with streamablehttp_client(url) as (read, write, _):
|
||||
async with streamable_http_client(url) as (read, write, _):
|
||||
async with ClientSession(
|
||||
read,
|
||||
write,
|
||||
|
||||
@@ -20,7 +20,7 @@ import logging
|
||||
|
||||
import click
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,7 +34,7 @@ async def run_demo(url: str, items: int, checkpoint_every: int) -> None:
|
||||
print(f"Processing {items} items with checkpoints every {checkpoint_every}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
async with streamablehttp_client(url) as (read_stream, write_stream, _):
|
||||
async with streamable_http_client(url) as (read_stream, write_stream, _):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
# Initialize the connection
|
||||
print("Initializing connection...")
|
||||
|
||||
+10
-2
@@ -93,7 +93,13 @@ packages = ["src/mcp"]
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "strict"
|
||||
include = ["src/mcp", "tests", "examples/servers", "examples/snippets"]
|
||||
include = [
|
||||
"src/mcp",
|
||||
"tests",
|
||||
"examples/servers",
|
||||
"examples/snippets",
|
||||
"examples/clients",
|
||||
]
|
||||
venvPath = "."
|
||||
venv = ".venv"
|
||||
# The FastAPI style of using decorators in tests gives a `reportUnusedFunction` error.
|
||||
@@ -102,7 +108,9 @@ venv = ".venv"
|
||||
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
|
||||
# and refactor code that is not public.
|
||||
executionEnvironments = [
|
||||
{ root = "tests", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false },
|
||||
{ root = "tests", extraPaths = [
|
||||
".",
|
||||
], reportUnusedFunction = false, reportPrivateUsage = false },
|
||||
{ root = "examples/servers", reportUnusedFunction = false },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user