Use 120 characters instead of 88 (#856)

This commit is contained in:
Marcelo Trylesinski 2025-06-11 02:45:50 -07:00 • committed by GitHub
commit 543961968c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 687 additions and 2142 deletions

View file

@ -24,9 +24,7 @@ logger = logging.getLogger("client")
async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
) -> None:
if isinstance(message, Exception):
logger.error("Error: %s", message)
@ -60,9 +58,7 @@ async def main(command_or_url: str, args: list[str], env: list[tuple[str, str]])
await run_session(*streams)
else:
# Use stdio client for commands
server_parameters = StdioServerParameters(
command=command_or_url, args=args, env=env_dict
)
server_parameters = StdioServerParameters(command=command_or_url, args=args, env=env_dict)
async with stdio_client(server_parameters) as streams:
await run_session(*streams)

View file

@ -17,12 +17,7 @@ from urllib.parse import urlencode, urljoin
import anyio
import httpx
from mcp.shared.auth import (
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthMetadata,
OAuthToken,
)
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata, OAuthToken
from mcp.types import LATEST_PROTOCOL_VERSION
logger = logging.getLogger(__name__)
@ -100,10 +95,7 @@ class OAuthClientProvider(httpx.Auth):
def _generate_code_verifier(self) -> str:
"""Generate a cryptographically random code verifier for PKCE."""
return "".join(
secrets.choice(string.ascii_letters + string.digits + "-._~")
for _ in range(128)
)
return "".join(secrets.choice(string.ascii_letters + string.digits + "-._~") for _ in range(128))
def _generate_code_challenge(self, code_verifier: str) -> str:
"""Generate a code challenge from a code verifier using SHA256."""
@ -148,9 +140,7 @@ class OAuthClientProvider(httpx.Auth):
return None
response.raise_for_status()
metadata_json = response.json()
logger.debug(
f"OAuth metadata discovered (no MCP header): {metadata_json}"
)
logger.debug(f"OAuth metadata discovered (no MCP header): {metadata_json}")
return OAuthMetadata.model_validate(metadata_json)
except Exception:
logger.exception("Failed to discover OAuth metadata")
@ -176,17 +166,11 @@ class OAuthClientProvider(httpx.Auth):
registration_url = urljoin(auth_base_url, "/register")
# Handle default scope
if (
client_metadata.scope is None
and metadata
and metadata.scopes_supported is not None
):
if client_metadata.scope is None and metadata and metadata.scopes_supported is not None:
client_metadata.scope = " ".join(metadata.scopes_supported)
# Serialize client metadata
registration_data = client_metadata.model_dump(
by_alias=True, mode="json", exclude_none=True
)
registration_data = client_metadata.model_dump(by_alias=True, mode="json", exclude_none=True)
async with httpx.AsyncClient() as client:
try:
@ -213,9 +197,7 @@ class OAuthClientProvider(httpx.Auth):
logger.exception("Registration error")
raise
async def async_auth_flow(
self, request: httpx.Request
) -> AsyncGenerator[httpx.Request, httpx.Response]:
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
"""
HTTPX auth flow integration.
"""
@ -225,9 +207,7 @@ class OAuthClientProvider(httpx.Auth):
await self.ensure_token()
# Add Bearer token if available
if self._current_tokens and self._current_tokens.access_token:
request.headers["Authorization"] = (
f"Bearer {self._current_tokens.access_token}"
)
request.headers["Authorization"] = f"Bearer {self._current_tokens.access_token}"
response = yield request
@ -305,11 +285,7 @@ class OAuthClientProvider(httpx.Auth):
return
# Try refreshing existing token
if (
self._current_tokens
and self._current_tokens.refresh_token
and await self._refresh_access_token()
):
if self._current_tokens and self._current_tokens.refresh_token and await self._refresh_access_token():
return
# Fall back to full OAuth flow
@ -361,12 +337,8 @@ class OAuthClientProvider(httpx.Auth):
auth_code, returned_state = await self.callback_handler()
# Validate state parameter for CSRF protection
if returned_state is None or not secrets.compare_digest(
returned_state, self._auth_state
):
raise Exception(
f"State parameter mismatch: {returned_state} != {self._auth_state}"
)
if returned_state is None or not secrets.compare_digest(returned_state, self._auth_state):
raise Exception(f"State parameter mismatch: {returned_state} != {self._auth_state}")
# Clear state after validation
self._auth_state = None
@ -377,9 +349,7 @@ class OAuthClientProvider(httpx.Auth):
# Exchange authorization code for tokens
await self._exchange_code_for_token(auth_code, client_info)
async def _exchange_code_for_token(
self, auth_code: str, client_info: OAuthClientInformationFull
) -> None:
async def _exchange_code_for_token(self, auth_code: str, client_info: OAuthClientInformationFull) -> None:
"""Exchange authorization code for access token."""
# Get token endpoint
if self._metadata and self._metadata.token_endpoint:
@ -412,17 +382,10 @@ class OAuthClientProvider(httpx.Auth):
# Parse OAuth error response
try:
error_data = response.json()
error_msg = error_data.get(
"error_description", error_data.get("error", "Unknown error")
)
raise Exception(
f"Token exchange failed: {error_msg} "
f"(HTTP {response.status_code})"
)
error_msg = error_data.get("error_description", error_data.get("error", "Unknown error"))
raise Exception(f"Token exchange failed: {error_msg} " f"(HTTP {response.status_code})")
except Exception:
raise Exception(
f"Token exchange failed: {response.status_code} {response.text}"
)
raise Exception(f"Token exchange failed: {response.status_code} {response.text}")
# Parse token response
token_response = OAuthToken.model_validate(response.json())

View file

@ -38,16 +38,12 @@ class LoggingFnT(Protocol):
class MessageHandlerFnT(Protocol):
async def __call__(
self,
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
) -> None: ...
async def _default_message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
) -> None:
await anyio.lowlevel.checkpoint()
@ -77,9 +73,7 @@ async def _default_logging_callback(
pass
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(
types.ClientResult | types.ErrorData
)
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)
class ClientSession(
@ -116,11 +110,7 @@ class ClientSession(
self._message_handler = message_handler or _default_message_handler
async def initialize(self) -> types.InitializeResult:
sampling = (
types.SamplingCapability()
if self._sampling_callback is not _default_sampling_callback
else None
)
sampling = types.SamplingCapability() if self._sampling_callback is not _default_sampling_callback else None
roots = (
# TODO: Should this be based on whether we
# _will_ send notifications, or only whether
@ -149,15 +139,10 @@ class ClientSession(
)
if result.protocolVersion not in SUPPORTED_PROTOCOL_VERSIONS:
raise RuntimeError(
"Unsupported protocol version from the server: "
f"{result.protocolVersion}"
)
raise RuntimeError("Unsupported protocol version from the server: " f"{result.protocolVersion}")
await self.send_notification(
types.ClientNotification(
types.InitializedNotification(method="notifications/initialized")
)
types.ClientNotification(types.InitializedNotification(method="notifications/initialized"))
)
return result
@ -207,33 +192,25 @@ class ClientSession(
types.EmptyResult,
)
async def list_resources(
self, cursor: str | None = None
) -> types.ListResourcesResult:
async def list_resources(self, cursor: str | None = None) -> types.ListResourcesResult:
"""Send a resources/list request."""
return await self.send_request(
types.ClientRequest(
types.ListResourcesRequest(
method="resources/list",
params=types.PaginatedRequestParams(cursor=cursor)
if cursor is not None
else None,
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
)
),
types.ListResourcesResult,
)
async def list_resource_templates(
self, cursor: str | None = None
) -> types.ListResourceTemplatesResult:
async def list_resource_templates(self, cursor: str | None = None) -> types.ListResourceTemplatesResult:
"""Send a resources/templates/list request."""
return await self.send_request(
types.ClientRequest(
types.ListResourceTemplatesRequest(
method="resources/templates/list",
params=types.PaginatedRequestParams(cursor=cursor)
if cursor is not None
else None,
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
)
),
types.ListResourceTemplatesResult,
@ -305,17 +282,13 @@ class ClientSession(
types.ClientRequest(
types.ListPromptsRequest(
method="prompts/list",
params=types.PaginatedRequestParams(cursor=cursor)
if cursor is not None
else None,
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
)
),
types.ListPromptsResult,
)
async def get_prompt(
self, name: str, arguments: dict[str, str] | None = None
) -> types.GetPromptResult:
async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
"""Send a prompts/get request."""
return await self.send_request(
types.ClientRequest(
@ -352,9 +325,7 @@ class ClientSession(
types.ClientRequest(
types.ListToolsRequest(
method="tools/list",
params=types.PaginatedRequestParams(cursor=cursor)
if cursor is not None
else None,
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
)
),
types.ListToolsResult,
@ -370,9 +341,7 @@ class ClientSession(
)
)
async def _received_request(
self, responder: RequestResponder[types.ServerRequest, types.ClientResult]
) -> None:
async def _received_request(self, responder: RequestResponder[types.ServerRequest, types.ClientResult]) -> None:
ctx = RequestContext[ClientSession, Any](
request_id=responder.request_id,
meta=responder.request_meta,
@ -395,22 +364,16 @@ class ClientSession(
case types.PingRequest():
with responder:
return await responder.respond(
types.ClientResult(root=types.EmptyResult())
)
return await responder.respond(types.ClientResult(root=types.EmptyResult()))
async def _handle_incoming(
self,
req: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
req: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
) -> None:
"""Handle incoming messages by forwarding to the message handler."""
await self._message_handler(req)
async def _received_notification(
self, notification: types.ServerNotification
) -> None:
async def _received_notification(self, notification: types.ServerNotification) -> None:
"""Handle notifications from the server."""
# Process specific notification types
match notification.root:

View file

@ -62,9 +62,7 @@ class StreamableHttpParameters(BaseModel):
terminate_on_close: bool = True
ServerParameters: TypeAlias = (
StdioServerParameters | SseServerParameters | StreamableHttpParameters
)
ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters
class ClientSessionGroup:
@ -261,9 +259,7 @@ class ClientSessionGroup:
)
read, write, _ = await session_stack.enter_async_context(client)
session = await session_stack.enter_async_context(
mcp.ClientSession(read, write)
)
session = await session_stack.enter_async_context(mcp.ClientSession(read, write))
result = await session.initialize()
# Session successfully initialized.
@ -280,9 +276,7 @@ class ClientSessionGroup:
await session_stack.aclose()
raise
async def _aggregate_components(
self, server_info: types.Implementation, session: mcp.ClientSession
) -> None:
async def _aggregate_components(self, server_info: types.Implementation, session: mcp.ClientSession) -> None:
"""Aggregates prompts, resources, and tools from a given session."""
# Create a reverse index so we can find all prompts, resources, and

View file

@ -73,20 +73,16 @@ async def sse_client(
match sse.event:
case "endpoint":
endpoint_url = urljoin(url, sse.data)
logger.debug(
f"Received endpoint URL: {endpoint_url}"
)
logger.debug(f"Received endpoint URL: {endpoint_url}")
url_parsed = urlparse(url)
endpoint_parsed = urlparse(endpoint_url)
if (
url_parsed.netloc != endpoint_parsed.netloc
or url_parsed.scheme
!= endpoint_parsed.scheme
or url_parsed.scheme != endpoint_parsed.scheme
):
error_msg = (
"Endpoint origin does not match "
f"connection origin: {endpoint_url}"
"Endpoint origin does not match " f"connection origin: {endpoint_url}"
)
logger.error(error_msg)
raise ValueError(error_msg)
@ -98,22 +94,16 @@ async def sse_client(
message = types.JSONRPCMessage.model_validate_json( # noqa: E501
sse.data
)
logger.debug(
f"Received server message: {message}"
)
logger.debug(f"Received server message: {message}")
except Exception as exc:
logger.error(
f"Error parsing server message: {exc}"
)
logger.error(f"Error parsing server message: {exc}")
await read_stream_writer.send(exc)
continue
session_message = SessionMessage(message)
await read_stream_writer.send(session_message)
case _:
logger.warning(
f"Unknown SSE event: {sse.event}"
)
logger.warning(f"Unknown SSE event: {sse.event}")
except Exception as exc:
logger.error(f"Error in sse_reader: {exc}")
await read_stream_writer.send(exc)
@ -124,9 +114,7 @@ async def sse_client(
try:
async with write_stream_reader:
async for session_message in write_stream_reader:
logger.debug(
f"Sending client message: {session_message}"
)
logger.debug(f"Sending client message: {session_message}")
response = await client.post(
endpoint_url,
json=session_message.message.model_dump(
@ -136,19 +124,14 @@ async def sse_client(
),
)
response.raise_for_status()
logger.debug(
"Client message sent successfully: "
f"{response.status_code}"
)
logger.debug("Client message sent successfully: " f"{response.status_code}")
except Exception as exc:
logger.error(f"Error in post_writer: {exc}")
finally:
await write_stream.aclose()
endpoint_url = await tg.start(sse_reader)
logger.debug(
f"Starting post writer with endpoint URL: {endpoint_url}"
)
logger.debug(f"Starting post writer with endpoint URL: {endpoint_url}")
tg.start_soon(post_writer, endpoint_url)
try:

View file

@ -115,11 +115,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
process = await _create_platform_compatible_process(
command=command,
args=server.args,
env=(
{**get_default_environment(), **server.env}
if server.env is not None
else get_default_environment()
),
env=({**get_default_environment(), **server.env} if server.env is not None else get_default_environment()),
errlog=errlog,
cwd=server.cwd,
)
@ -163,9 +159,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
try:
async with write_stream_reader:
async for session_message in write_stream_reader:
json = session_message.message.model_dump_json(
by_alias=True, exclude_none=True
)
json = session_message.message.model_dump_json(by_alias=True, exclude_none=True)
await process.stdin.send(
(json + "\n").encode(
encoding=server.encoding,
@ -229,8 +223,6 @@ async def _create_platform_compatible_process(
if sys.platform == "win32":
process = await create_windows_process(command, args, env, errlog, cwd)
else:
process = await anyio.open_process(
[command, *args], env=env, stderr=errlog, cwd=cwd
)
process = await anyio.open_process([command, *args], env=env, stderr=errlog, cwd=cwd)
return process

View file

@ -82,9 +82,7 @@ async def create_windows_process(
return process
except Exception:
# Don't raise, let's try to create the process without creation flags
process = await anyio.open_process(
[command, *args], env=env, stderr=errlog, cwd=cwd
)
process = await anyio.open_process([command, *args], env=env, stderr=errlog, cwd=cwd)
return process

View file

@ -106,9 +106,7 @@ class StreamableHTTPTransport:
**self.headers,
}
def _update_headers_with_session(
self, base_headers: dict[str, str]
) -> dict[str, str]:
def _update_headers_with_session(self, base_headers: dict[str, str]) -> dict[str, str]:
"""Update headers with session ID if available."""
headers = base_headers.copy()
if self.session_id:
@ -117,17 +115,11 @@ class StreamableHTTPTransport:
def _is_initialization_request(self, message: JSONRPCMessage) -> bool:
"""Check if the message is an initialization request."""
return (
isinstance(message.root, JSONRPCRequest)
and message.root.method == "initialize"
)
return isinstance(message.root, JSONRPCRequest) and message.root.method == "initialize"
def _is_initialized_notification(self, message: JSONRPCMessage) -> bool:
"""Check if the message is an initialized notification."""
return (
isinstance(message.root, JSONRPCNotification)
and message.root.method == "notifications/initialized"
)
return isinstance(message.root, JSONRPCNotification) and message.root.method == "notifications/initialized"
def _maybe_extract_session_id_from_response(
self,
@ -153,9 +145,7 @@ class StreamableHTTPTransport:
logger.debug(f"SSE message: {message}")
# If this is a response and we have original_request_id, replace it
if original_request_id is not None and isinstance(
message.root, JSONRPCResponse | JSONRPCError
):
if original_request_id is not None and isinstance(message.root, JSONRPCResponse | JSONRPCError):
message.root.id = original_request_id
session_message = SessionMessage(message)
@ -227,7 +217,8 @@ class StreamableHTTPTransport:
self.url,
headers=headers,
timeout=httpx.Timeout(
self.timeout.total_seconds(), read=ctx.sse_read_timeout.total_seconds()
self.timeout.total_seconds(),
read=ctx.sse_read_timeout.total_seconds(),
),
) as event_source:
event_source.response.raise_for_status()
@ -298,9 +289,7 @@ class StreamableHTTPTransport:
logger.error(f"Error parsing JSON response: {exc}")
await read_stream_writer.send(exc)
async def _handle_sse_response(
self, response: httpx.Response, ctx: RequestContext
) -> None:
async def _handle_sse_response(self, response: httpx.Response, ctx: RequestContext) -> None:
"""Handle SSE response from the server."""
try:
event_source = EventSource(response)
@ -308,11 +297,7 @@ class StreamableHTTPTransport:
is_complete = await self._handle_sse_event(
sse,
ctx.read_stream_writer,
resumption_callback=(
ctx.metadata.on_resumption_token_update
if ctx.metadata
else None
),
resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None),
)
# If the SSE event indicates completion, like returning respose/error
# break the loop
@ -455,12 +440,8 @@ async def streamablehttp_client(
"""
transport = StreamableHTTPTransport(url, headers, timeout, sse_read_timeout, auth)
read_stream_writer, read_stream = anyio.create_memory_object_stream[
SessionMessage | Exception
](0)
write_stream, write_stream_reader = anyio.create_memory_object_stream[
SessionMessage
](0)
read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0)
write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0)
async with anyio.create_task_group() as tg:
try:
@ -476,9 +457,7 @@ async def streamablehttp_client(
) as client:
# Define callbacks that need access to tg
def start_get_stream() -> None:
tg.start_soon(
transport.handle_get_stream, client, read_stream_writer
)
tg.start_soon(transport.handle_get_stream, client, read_stream_writer)
tg.start_soon(
transport.post_writer,

View file

@ -19,10 +19,7 @@ logger = logging.getLogger(__name__)
async def websocket_client(
url: str,
) -> AsyncGenerator[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
],
tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]],
None,
]:
"""
@ -74,9 +71,7 @@ async def websocket_client(
async with write_stream_reader:
async for session_message in write_stream_reader:
# Convert to a dict, then to JSON
msg_dict = session_message.message.model_dump(
by_alias=True, mode="json", exclude_none=True
)
msg_dict = session_message.message.model_dump(by_alias=True, mode="json", exclude_none=True)
await ws.send(json.dumps(msg_dict))
async with anyio.create_task_group() as tg: