ci: add strict-no-cover to detect unnecessary coverage pragmas (#1897)
This commit is contained in:
parent
3cfdea0e97
commit
a7ddfdae07
54 changed files with 355 additions and 358 deletions
4
.github/workflows/shared.yml
vendored
4
.github/workflows/shared.yml
vendored
|
|
@ -62,6 +62,10 @@ jobs:
|
||||||
uv run --frozen --no-sync coverage combine
|
uv run --frozen --no-sync coverage combine
|
||||||
uv run --frozen --no-sync coverage report
|
uv run --frozen --no-sync coverage report
|
||||||
|
|
||||||
|
- name: Check for unnecessary no cover pragmas
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
run: uv run --frozen --no-sync strict-no-cover
|
||||||
|
|
||||||
readme-snippets:
|
readme-snippets:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,9 @@ dev = [
|
||||||
"pytest-pretty>=1.2.0",
|
"pytest-pretty>=1.2.0",
|
||||||
"inline-snapshot>=0.23.0",
|
"inline-snapshot>=0.23.0",
|
||||||
"dirty-equals>=0.9.0",
|
"dirty-equals>=0.9.0",
|
||||||
"coverage[toml]>=7.13.1",
|
"coverage[toml]>=7.10.7,<=7.13",
|
||||||
"pillow>=12.0",
|
"pillow>=12.0",
|
||||||
|
"strict-no-cover",
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"mkdocs>=1.6.1",
|
"mkdocs>=1.6.1",
|
||||||
|
|
@ -163,6 +164,7 @@ members = ["examples/clients/*", "examples/servers/*", "examples/snippets"]
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
mcp = { workspace = true }
|
mcp = { workspace = true }
|
||||||
|
strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" }
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
log_cli = true
|
log_cli = true
|
||||||
|
|
@ -198,7 +200,6 @@ branch = true
|
||||||
patch = ["subprocess"]
|
patch = ["subprocess"]
|
||||||
concurrency = ["multiprocessing", "thread"]
|
concurrency = ["multiprocessing", "thread"]
|
||||||
source = ["src", "tests"]
|
source = ["src", "tests"]
|
||||||
relative_files = true
|
|
||||||
omit = [
|
omit = [
|
||||||
"src/mcp/client/__main__.py",
|
"src/mcp/client/__main__.py",
|
||||||
"src/mcp/server/__main__.py",
|
"src/mcp/server/__main__.py",
|
||||||
|
|
@ -215,6 +216,7 @@ ignore_errors = true
|
||||||
precision = 2
|
precision = 2
|
||||||
exclude_lines = [
|
exclude_lines = [
|
||||||
"pragma: no cover",
|
"pragma: no cover",
|
||||||
|
"pragma: lax no cover",
|
||||||
"if TYPE_CHECKING:",
|
"if TYPE_CHECKING:",
|
||||||
"@overload",
|
"@overload",
|
||||||
"raise NotImplementedError",
|
"raise NotImplementedError",
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ def update_claude_config(
|
||||||
)
|
)
|
||||||
|
|
||||||
config_file = config_dir / "claude_desktop_config.json"
|
config_file = config_dir / "claude_desktop_config.json"
|
||||||
if not config_file.exists(): # pragma: no cover
|
if not config_file.exists(): # pragma: lax no cover
|
||||||
try:
|
try:
|
||||||
config_file.write_text("{}")
|
config_file.write_text("{}")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ def _build_uv_command(
|
||||||
|
|
||||||
if with_packages:
|
if with_packages:
|
||||||
for pkg in with_packages:
|
for pkg in with_packages:
|
||||||
if pkg: # pragma: no cover
|
if pkg: # pragma: no branch
|
||||||
cmd.extend(["--with", pkg])
|
cmd.extend(["--with", pkg])
|
||||||
|
|
||||||
# Add mcp run command
|
# Add mcp run command
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ class OAuthContext:
|
||||||
headers = {} # pragma: no cover
|
headers = {} # pragma: no cover
|
||||||
|
|
||||||
if not self.client_info:
|
if not self.client_info:
|
||||||
return data, headers # pragma: no cover
|
return data, headers
|
||||||
|
|
||||||
auth_method = self.client_info.token_endpoint_auth_method
|
auth_method = self.client_info.token_endpoint_auth_method
|
||||||
|
|
||||||
|
|
@ -418,7 +418,7 @@ class OAuthClientProvider(httpx.Auth):
|
||||||
raise OAuthTokenError("No client info available") # pragma: no cover
|
raise OAuthTokenError("No client info available") # pragma: no cover
|
||||||
|
|
||||||
if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint:
|
if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint:
|
||||||
token_url = str(self.context.oauth_metadata.token_endpoint) # pragma: no cover
|
token_url = str(self.context.oauth_metadata.token_endpoint)
|
||||||
else:
|
else:
|
||||||
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
|
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
|
||||||
token_url = urljoin(auth_base_url, "/token")
|
token_url = urljoin(auth_base_url, "/token")
|
||||||
|
|
@ -534,7 +534,7 @@ class OAuthClientProvider(httpx.Auth):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers)
|
# Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers)
|
||||||
for url in asm_discovery_urls: # pragma: no cover
|
for url in asm_discovery_urls: # pragma: no branch
|
||||||
oauth_metadata_request = create_oauth_metadata_request(url)
|
oauth_metadata_request = create_oauth_metadata_request(url)
|
||||||
oauth_metadata_response = yield oauth_metadata_request
|
oauth_metadata_response = yield oauth_metadata_request
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ class ClientSession(
|
||||||
meta: RequestParamsMeta | None = None,
|
meta: RequestParamsMeta | None = None,
|
||||||
) -> types.EmptyResult:
|
) -> types.EmptyResult:
|
||||||
"""Send a logging/setLevel request."""
|
"""Send a logging/setLevel request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request(
|
||||||
types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)),
|
types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)),
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
@ -285,14 +285,14 @@ class ClientSession(
|
||||||
|
|
||||||
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
||||||
"""Send a resources/subscribe request."""
|
"""Send a resources/subscribe request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request(
|
||||||
types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)),
|
types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)),
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
||||||
"""Send a resources/unsubscribe request."""
|
"""Send a resources/unsubscribe request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request(
|
||||||
types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)),
|
types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)),
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
@ -344,7 +344,7 @@ class ClientSession(
|
||||||
try:
|
try:
|
||||||
validate(result.structured_content, output_schema)
|
validate(result.structured_content, output_schema)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") # pragma: no cover
|
raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}")
|
||||||
except SchemaError as e: # pragma: no cover
|
except SchemaError as e: # pragma: no cover
|
||||||
raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover
|
raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,22 +223,22 @@ class ClientSessionGroup:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if session_known_for_components: # pragma: no cover
|
if session_known_for_components: # pragma: no branch
|
||||||
component_names = self._sessions.pop(session) # Pop from _sessions tracking
|
component_names = self._sessions.pop(session) # Pop from _sessions tracking
|
||||||
|
|
||||||
# Remove prompts associated with the session.
|
# Remove prompts associated with the session.
|
||||||
for name in component_names.prompts:
|
for name in component_names.prompts:
|
||||||
if name in self._prompts:
|
if name in self._prompts: # pragma: no branch
|
||||||
del self._prompts[name]
|
del self._prompts[name]
|
||||||
# Remove resources associated with the session.
|
# Remove resources associated with the session.
|
||||||
for name in component_names.resources:
|
for name in component_names.resources:
|
||||||
if name in self._resources:
|
if name in self._resources: # pragma: no branch
|
||||||
del self._resources[name]
|
del self._resources[name]
|
||||||
# Remove tools associated with the session.
|
# Remove tools associated with the session.
|
||||||
for name in component_names.tools:
|
for name in component_names.tools:
|
||||||
if name in self._tools:
|
if name in self._tools: # pragma: no branch
|
||||||
del self._tools[name]
|
del self._tools[name]
|
||||||
if name in self._tool_to_session:
|
if name in self._tool_to_session: # pragma: no branch
|
||||||
del self._tool_to_session[name]
|
del self._tool_to_session[name]
|
||||||
|
|
||||||
# Clean up the session's resources via its dedicated exit stack
|
# Clean up the session's resources via its dedicated exit stack
|
||||||
|
|
|
||||||
|
|
@ -119,12 +119,12 @@ async def sse_client(
|
||||||
await read_stream_writer.send(session_message)
|
await read_stream_writer.send(session_message)
|
||||||
case _: # pragma: no cover
|
case _: # pragma: no cover
|
||||||
logger.warning(f"Unknown SSE event: {sse.event}") # pragma: no cover
|
logger.warning(f"Unknown SSE event: {sse.event}") # pragma: no cover
|
||||||
except SSEError as sse_exc: # pragma: no cover
|
except SSEError as sse_exc: # pragma: lax no cover
|
||||||
logger.exception("Encountered SSE exception") # pragma: no cover
|
logger.exception("Encountered SSE exception")
|
||||||
raise sse_exc # pragma: no cover
|
raise sse_exc
|
||||||
except Exception as exc: # pragma: no cover
|
except Exception as exc: # pragma: lax no cover
|
||||||
logger.exception("Error in sse_reader") # pragma: no cover
|
logger.exception("Error in sse_reader")
|
||||||
await read_stream_writer.send(exc) # pragma: no cover
|
await read_stream_writer.send(exc)
|
||||||
finally:
|
finally:
|
||||||
await read_stream_writer.aclose()
|
await read_stream_writer.aclose()
|
||||||
|
|
||||||
|
|
@ -143,8 +143,8 @@ async def sse_client(
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.debug(f"Client message sent successfully: {response.status_code}")
|
logger.debug(f"Client message sent successfully: {response.status_code}")
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: lax no cover
|
||||||
logger.exception("Error in post_writer") # pragma: no cover
|
logger.exception("Error in post_writer")
|
||||||
finally:
|
finally:
|
||||||
await write_stream.aclose()
|
await write_stream.aclose()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ def get_default_environment() -> dict[str, str]:
|
||||||
|
|
||||||
for key in DEFAULT_INHERITED_ENV_VARS:
|
for key in DEFAULT_INHERITED_ENV_VARS:
|
||||||
value = os.environ.get(key)
|
value = os.environ.get(key)
|
||||||
if value is None:
|
if value is None: # pragma: lax no cover
|
||||||
continue # pragma: no cover
|
continue
|
||||||
|
|
||||||
if value.startswith("()"): # pragma: no cover
|
if value.startswith("()"): # pragma: no cover
|
||||||
# Skip functions, which are a security risk
|
# Skip functions, which are a security risk
|
||||||
|
|
@ -158,7 +158,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
|
||||||
|
|
||||||
session_message = SessionMessage(message)
|
session_message = SessionMessage(message)
|
||||||
await read_stream_writer.send(session_message)
|
await read_stream_writer.send(session_message)
|
||||||
except anyio.ClosedResourceError: # pragma: no cover
|
except anyio.ClosedResourceError: # pragma: lax no cover
|
||||||
await anyio.lowlevel.checkpoint()
|
await anyio.lowlevel.checkpoint()
|
||||||
|
|
||||||
async def stdin_writer():
|
async def stdin_writer():
|
||||||
|
|
@ -225,8 +225,8 @@ def _get_executable_command(command: str) -> str:
|
||||||
"""
|
"""
|
||||||
if sys.platform == "win32": # pragma: no cover
|
if sys.platform == "win32": # pragma: no cover
|
||||||
return get_windows_executable_command(command)
|
return get_windows_executable_command(command)
|
||||||
else:
|
else: # pragma: lax no cover
|
||||||
return command # pragma: no cover
|
return command
|
||||||
|
|
||||||
|
|
||||||
async def _create_platform_compatible_process(
|
async def _create_platform_compatible_process(
|
||||||
|
|
@ -243,14 +243,14 @@ async def _create_platform_compatible_process(
|
||||||
"""
|
"""
|
||||||
if sys.platform == "win32": # pragma: no cover
|
if sys.platform == "win32": # pragma: no cover
|
||||||
process = await create_windows_process(command, args, env, errlog, cwd)
|
process = await create_windows_process(command, args, env, errlog, cwd)
|
||||||
else:
|
else: # pragma: lax no cover
|
||||||
process = await anyio.open_process(
|
process = await anyio.open_process(
|
||||||
[command, *args],
|
[command, *args],
|
||||||
env=env,
|
env=env,
|
||||||
stderr=errlog,
|
stderr=errlog,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
start_new_session=True,
|
start_new_session=True,
|
||||||
) # pragma: no cover
|
)
|
||||||
|
|
||||||
return process
|
return process
|
||||||
|
|
||||||
|
|
@ -267,7 +267,7 @@ async def _terminate_process_tree(process: Process | FallbackProcess, timeout_se
|
||||||
"""
|
"""
|
||||||
if sys.platform == "win32": # pragma: no cover
|
if sys.platform == "win32": # pragma: no cover
|
||||||
await terminate_windows_process_tree(process, timeout_seconds)
|
await terminate_windows_process_tree(process, timeout_seconds)
|
||||||
else: # pragma: no cover
|
else: # pragma: lax no cover
|
||||||
# FallbackProcess should only be used for Windows compatibility
|
# FallbackProcess should only be used for Windows compatibility
|
||||||
assert isinstance(process, Process)
|
assert isinstance(process, Process)
|
||||||
await terminate_posix_process_tree(process, timeout_seconds)
|
await terminate_posix_process_tree(process, timeout_seconds)
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ class StreamableHTTPTransport:
|
||||||
|
|
||||||
headers = self._prepare_headers()
|
headers = self._prepare_headers()
|
||||||
if last_event_id:
|
if last_event_id:
|
||||||
headers[LAST_EVENT_ID] = last_event_id # pragma: no cover
|
headers[LAST_EVENT_ID] = last_event_id
|
||||||
|
|
||||||
async with aconnect_sse(client, "GET", self.url, headers=headers) as event_source:
|
async with aconnect_sse(client, "GET", self.url, headers=headers) as event_source:
|
||||||
event_source.response.raise_for_status()
|
event_source.response.raise_for_status()
|
||||||
|
|
@ -190,17 +190,17 @@ class StreamableHTTPTransport:
|
||||||
async for sse in event_source.aiter_sse():
|
async for sse in event_source.aiter_sse():
|
||||||
# Track last event ID for reconnection
|
# Track last event ID for reconnection
|
||||||
if sse.id:
|
if sse.id:
|
||||||
last_event_id = sse.id # pragma: no cover
|
last_event_id = sse.id
|
||||||
# Track retry interval from server
|
# Track retry interval from server
|
||||||
if sse.retry is not None:
|
if sse.retry is not None:
|
||||||
retry_interval_ms = sse.retry # pragma: no cover
|
retry_interval_ms = sse.retry
|
||||||
|
|
||||||
await self._handle_sse_event(sse, read_stream_writer)
|
await self._handle_sse_event(sse, read_stream_writer)
|
||||||
|
|
||||||
# Stream ended normally (server closed) - reset attempt counter
|
# Stream ended normally (server closed) - reset attempt counter
|
||||||
attempt = 0
|
attempt = 0
|
||||||
|
|
||||||
except Exception as exc: # pragma: no cover
|
except Exception as exc: # pragma: lax no cover
|
||||||
logger.debug(f"GET stream error: {exc}")
|
logger.debug(f"GET stream error: {exc}")
|
||||||
attempt += 1
|
attempt += 1
|
||||||
|
|
||||||
|
|
@ -333,8 +333,8 @@ class StreamableHTTPTransport:
|
||||||
if is_complete:
|
if is_complete:
|
||||||
await response.aclose()
|
await response.aclose()
|
||||||
return # Normal completion, no reconnect needed
|
return # Normal completion, no reconnect needed
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e:
|
||||||
logger.debug(f"SSE stream ended: {e}")
|
logger.debug(f"SSE stream ended: {e}") # pragma: no cover
|
||||||
|
|
||||||
# Stream ended without response - reconnect if we received an event with ID
|
# Stream ended without response - reconnect if we received an event with ID
|
||||||
if last_event_id is not None: # pragma: no branch
|
if last_event_id is not None: # pragma: no branch
|
||||||
|
|
@ -472,20 +472,20 @@ class StreamableHTTPTransport:
|
||||||
await read_stream_writer.aclose()
|
await read_stream_writer.aclose()
|
||||||
await write_stream.aclose()
|
await write_stream.aclose()
|
||||||
|
|
||||||
async def terminate_session(self, client: httpx.AsyncClient) -> None: # pragma: no cover
|
async def terminate_session(self, client: httpx.AsyncClient) -> None:
|
||||||
"""Terminate the session by sending a DELETE request."""
|
"""Terminate the session by sending a DELETE request."""
|
||||||
if not self.session_id:
|
if not self.session_id: # pragma: lax no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = self._prepare_headers()
|
headers = self._prepare_headers()
|
||||||
response = await client.delete(self.url, headers=headers)
|
response = await client.delete(self.url, headers=headers)
|
||||||
|
|
||||||
if response.status_code == 405:
|
if response.status_code == 405: # pragma: lax no cover
|
||||||
logger.debug("Server does not allow session termination")
|
logger.debug("Server does not allow session termination")
|
||||||
elif response.status_code not in (200, 204):
|
elif response.status_code not in (200, 204): # pragma: lax no cover
|
||||||
logger.warning(f"Session termination failed: {response.status_code}")
|
logger.warning(f"Session termination failed: {response.status_code}")
|
||||||
except Exception as exc:
|
except Exception as exc: # pragma: no cover
|
||||||
logger.warning(f"Session termination failed: {exc}")
|
logger.warning(f"Session termination failed: {exc}")
|
||||||
|
|
||||||
def get_session_id(self) -> str | None:
|
def get_session_id(self) -> str | None:
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ class TokenHandler:
|
||||||
except TokenError as e:
|
except TokenError as e:
|
||||||
return self.response(TokenErrorResponse(error=e.error, error_description=e.error_description))
|
return self.response(TokenErrorResponse(error=e.error, error_description=e.error_description))
|
||||||
|
|
||||||
case RefreshTokenRequest(): # pragma: no cover
|
case RefreshTokenRequest(): # pragma: no branch
|
||||||
refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)
|
refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)
|
||||||
if refresh_token is None or refresh_token.client_id != token_request.client_id:
|
if refresh_token is None or refresh_token.client_id != token_request.client_id:
|
||||||
# if token belongs to different client, pretend it doesn't exist
|
# if token belongs to different client, pretend it doesn't exist
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from mcp.shared.auth import OAuthClientInformationFull
|
||||||
|
|
||||||
class AuthenticationError(Exception):
|
class AuthenticationError(Exception):
|
||||||
def __init__(self, message: str):
|
def __init__(self, message: str):
|
||||||
self.message = message # pragma: no cover
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
class ClientAuthenticator:
|
class ClientAuthenticator:
|
||||||
|
|
@ -96,15 +96,15 @@ class ClientAuthenticator:
|
||||||
|
|
||||||
# If client from the store expects a secret, validate that the request provides
|
# If client from the store expects a secret, validate that the request provides
|
||||||
# that secret
|
# that secret
|
||||||
if client.client_secret: # pragma: no branch
|
if client.client_secret:
|
||||||
if not request_client_secret:
|
if not request_client_secret:
|
||||||
raise AuthenticationError("Client secret is required") # pragma: no cover
|
raise AuthenticationError("Client secret is required")
|
||||||
|
|
||||||
# hmac.compare_digest requires that both arguments are either bytes or a `str` containing
|
# hmac.compare_digest requires that both arguments are either bytes or a `str` containing
|
||||||
# only ASCII characters. Since we do not control `request_client_secret`, we encode both
|
# only ASCII characters. Since we do not control `request_client_secret`, we encode both
|
||||||
# arguments to bytes.
|
# arguments to bytes.
|
||||||
if not hmac.compare_digest(client.client_secret.encode(), request_client_secret.encode()):
|
if not hmac.compare_digest(client.client_secret.encode(), request_client_secret.encode()):
|
||||||
raise AuthenticationError("Invalid client_secret") # pragma: no cover
|
raise AuthenticationError("Invalid client_secret")
|
||||||
|
|
||||||
if client.client_secret_expires_at and client.client_secret_expires_at < int(time.time()):
|
if client.client_secret_expires_at and client.client_secret_expires_at < int(time.time()):
|
||||||
raise AuthenticationError("Client secret has expired") # pragma: no cover
|
raise AuthenticationError("Client secret has expired") # pragma: no cover
|
||||||
|
|
|
||||||
|
|
@ -247,8 +247,7 @@ class ServerTaskContext:
|
||||||
response_data = await resolver.wait()
|
response_data = await resolver.wait()
|
||||||
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
||||||
return ElicitResult.model_validate(response_data)
|
return ElicitResult.model_validate(response_data)
|
||||||
except anyio.get_cancelled_exc_class(): # pragma: no cover
|
except anyio.get_cancelled_exc_class():
|
||||||
# Coverage can't track async exception handlers reliably.
|
|
||||||
# This path is tested in test_elicit_restores_status_on_cancellation
|
# This path is tested in test_elicit_restores_status_on_cancellation
|
||||||
# which verifies status is restored to "working" after cancellation.
|
# which verifies status is restored to "working" after cancellation.
|
||||||
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
||||||
|
|
@ -408,8 +407,7 @@ class ServerTaskContext:
|
||||||
response_data = await resolver.wait()
|
response_data = await resolver.wait()
|
||||||
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
||||||
return CreateMessageResult.model_validate(response_data)
|
return CreateMessageResult.model_validate(response_data)
|
||||||
except anyio.get_cancelled_exc_class(): # pragma: no cover
|
except anyio.get_cancelled_exc_class():
|
||||||
# Coverage can't track async exception handlers reliably.
|
|
||||||
# This path is tested in test_create_message_restores_status_on_cancellation
|
# This path is tested in test_create_message_restores_status_on_cancellation
|
||||||
# which verifies status is restored to "working" after cancellation.
|
# which verifies status is restored to "working" after cancellation.
|
||||||
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ class FileResource(Resource):
|
||||||
|
|
||||||
@pydantic.field_validator("path")
|
@pydantic.field_validator("path")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_absolute_path(cls, path: Path) -> Path: # pragma: no cover
|
def validate_absolute_path(cls, path: Path) -> Path:
|
||||||
"""Ensure path is absolute."""
|
"""Ensure path is absolute."""
|
||||||
if not path.is_absolute():
|
if not path.is_absolute():
|
||||||
raise ValueError("Path must be absolute")
|
raise ValueError("Path must be absolute")
|
||||||
|
|
|
||||||
|
|
@ -873,10 +873,10 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes, resource_metadata_url),
|
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes, resource_metadata_url),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else: # pragma: no cover
|
else:
|
||||||
# Auth is disabled, no need for RequireAuthMiddleware
|
# Auth is disabled, no need for RequireAuthMiddleware
|
||||||
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
|
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
|
||||||
async def sse_endpoint(request: Request) -> Response:
|
async def sse_endpoint(request: Request) -> Response: # pragma: no cover
|
||||||
# Convert the Starlette request to ASGI parameters
|
# Convert the Starlette request to ASGI parameters
|
||||||
return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
|
return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ class Tool(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
def _is_async_callable(obj: Any) -> bool:
|
def _is_async_callable(obj: Any) -> bool:
|
||||||
while isinstance(obj, functools.partial): # pragma: no cover
|
while isinstance(obj, functools.partial): # pragma: lax no cover
|
||||||
obj = obj.func
|
obj = obj.func
|
||||||
|
|
||||||
return inspect.iscoroutinefunction(obj) or (
|
return inspect.iscoroutinefunction(obj) or (
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ def find_context_parameter(fn: Callable[..., Any]) -> str | None:
|
||||||
# Get type hints to properly resolve string annotations
|
# Get type hints to properly resolve string annotations
|
||||||
try:
|
try:
|
||||||
hints = typing.get_type_hints(fn)
|
hints = typing.get_type_hints(fn)
|
||||||
# TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed.
|
except Exception: # pragma: lax no cover
|
||||||
except Exception: # pragma: no cover
|
|
||||||
# If we can't resolve type hints, we can't find the context parameter
|
# If we can't resolve type hints, we can't find the context parameter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ def configure_logging(
|
||||||
level: the log level to use
|
level: the log level to use
|
||||||
"""
|
"""
|
||||||
handlers: list[logging.Handler] = []
|
handlers: list[logging.Handler] = []
|
||||||
try: # pragma: no cover
|
try:
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
tools_capability = types.ToolsCapability(list_changed=notification_options.tools_changed)
|
tools_capability = types.ToolsCapability(list_changed=notification_options.tools_changed)
|
||||||
|
|
||||||
# Set logging capabilities if handler exists
|
# Set logging capabilities if handler exists
|
||||||
if types.SetLevelRequest in self.request_handlers: # pragma: no cover
|
if types.SetLevelRequest in self.request_handlers:
|
||||||
logging_capability = types.LoggingCapability()
|
logging_capability = types.LoggingCapability()
|
||||||
|
|
||||||
# Set completions capabilities if handler exists
|
# Set completions capabilities if handler exists
|
||||||
|
|
@ -379,7 +379,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
mime_type=mime_type or "text/plain",
|
mime_type=mime_type or "text/plain",
|
||||||
**meta_kwargs,
|
**meta_kwargs,
|
||||||
)
|
)
|
||||||
case bytes() as data: # pragma: no cover
|
case bytes() as data: # pragma: no branch
|
||||||
return types.BlobResourceContents(
|
return types.BlobResourceContents(
|
||||||
uri=req.params.uri,
|
uri=req.params.uri,
|
||||||
blob=base64.b64encode(data).decode(),
|
blob=base64.b64encode(data).decode(),
|
||||||
|
|
@ -388,7 +388,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
)
|
)
|
||||||
|
|
||||||
match result:
|
match result:
|
||||||
case str() | bytes() as data: # pragma: no cover
|
case str() | bytes() as data: # pragma: lax no cover
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Returning str or bytes from read_resource is deprecated. "
|
"Returning str or bytes from read_resource is deprecated. "
|
||||||
"Use Iterable[ReadResourceContents] instead.",
|
"Use Iterable[ReadResourceContents] instead.",
|
||||||
|
|
@ -416,7 +416,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def set_logging_level(self): # pragma: no cover
|
def set_logging_level(self):
|
||||||
def decorator(func: Callable[[types.LoggingLevel], Awaitable[None]]):
|
def decorator(func: Callable[[types.LoggingLevel], Awaitable[None]]):
|
||||||
logger.debug("Registering handler for SetLevelRequest")
|
logger.debug("Registering handler for SetLevelRequest")
|
||||||
|
|
||||||
|
|
@ -429,7 +429,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def subscribe_resource(self): # pragma: no cover
|
def subscribe_resource(self):
|
||||||
def decorator(func: Callable[[str], Awaitable[None]]):
|
def decorator(func: Callable[[str], Awaitable[None]]):
|
||||||
logger.debug("Registering handler for SubscribeRequest")
|
logger.debug("Registering handler for SubscribeRequest")
|
||||||
|
|
||||||
|
|
@ -442,7 +442,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def unsubscribe_resource(self): # pragma: no cover
|
def unsubscribe_resource(self):
|
||||||
def decorator(func: Callable[[str], Awaitable[None]]):
|
def decorator(func: Callable[[str], Awaitable[None]]):
|
||||||
logger.debug("Registering handler for UnsubscribeRequest")
|
logger.debug("Registering handler for UnsubscribeRequest")
|
||||||
|
|
||||||
|
|
@ -468,7 +468,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
result = await wrapper(req)
|
result = await wrapper(req)
|
||||||
|
|
||||||
# Handle both old style (list[Tool]) and new style (ListToolsResult)
|
# Handle both old style (list[Tool]) and new style (ListToolsResult)
|
||||||
if isinstance(result, types.ListToolsResult): # pragma: no cover
|
if isinstance(result, types.ListToolsResult):
|
||||||
# Refresh the tool cache with returned tools
|
# Refresh the tool cache with returned tools
|
||||||
for tool in result.tools:
|
for tool in result.tools:
|
||||||
validate_and_warn_tool_name(tool.name)
|
validate_and_warn_tool_name(tool.name)
|
||||||
|
|
@ -571,7 +571,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
# tool returned structured content only
|
# tool returned structured content only
|
||||||
maybe_structured_content = cast(StructuredContent, results)
|
maybe_structured_content = cast(StructuredContent, results)
|
||||||
unstructured_content = [types.TextContent(type="text", text=json.dumps(results, indent=2))]
|
unstructured_content = [types.TextContent(type="text", text=json.dumps(results, indent=2))]
|
||||||
elif hasattr(results, "__iter__"): # pragma: no cover
|
elif hasattr(results, "__iter__"):
|
||||||
# tool returned unstructured content only
|
# tool returned unstructured content only
|
||||||
unstructured_content = cast(UnstructuredContent, results)
|
unstructured_content = cast(UnstructuredContent, results)
|
||||||
maybe_structured_content = None
|
maybe_structured_content = None
|
||||||
|
|
@ -714,7 +714,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
await self._handle_request(
|
await self._handle_request(
|
||||||
message, responder.request, session, lifespan_context, raise_exceptions
|
message, responder.request, session, lifespan_context, raise_exceptions
|
||||||
)
|
)
|
||||||
case Exception(): # pragma: no cover
|
case Exception():
|
||||||
logger.error(f"Received exception from stream: {message}")
|
logger.error(f"Received exception from stream: {message}")
|
||||||
await session.send_log_message(
|
await session.send_log_message(
|
||||||
level="error",
|
level="error",
|
||||||
|
|
@ -726,7 +726,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
case _:
|
case _:
|
||||||
await self._handle_notification(message)
|
await self._handle_notification(message)
|
||||||
|
|
||||||
for warning in w: # pragma: no cover
|
for warning in w: # pragma: lax no cover
|
||||||
logger.info("Warning: %s: %s", warning.category.__name__, warning.message)
|
logger.info("Warning: %s: %s", warning.category.__name__, warning.message)
|
||||||
|
|
||||||
async def _handle_request(
|
async def _handle_request(
|
||||||
|
|
@ -781,16 +781,16 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
response = await handler(req)
|
response = await handler(req)
|
||||||
except McpError as err: # pragma: no cover
|
except McpError as err:
|
||||||
response = err.error
|
response = err.error
|
||||||
except anyio.get_cancelled_exc_class(): # pragma: no cover
|
except anyio.get_cancelled_exc_class():
|
||||||
logger.info(
|
logger.info(
|
||||||
"Request %s cancelled - duplicate response suppressed",
|
"Request %s cancelled - duplicate response suppressed",
|
||||||
message.request_id,
|
message.request_id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except Exception as err: # pragma: no cover
|
except Exception as err:
|
||||||
if raise_exceptions:
|
if raise_exceptions: # pragma: no cover
|
||||||
raise err
|
raise err
|
||||||
response = types.ErrorData(code=0, message=str(err), data=None)
|
response = types.ErrorData(code=0, message=str(err), data=None)
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ class ServerSession(
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client_params(self) -> types.InitializeRequestParams | None:
|
def client_params(self) -> types.InitializeRequestParams | None:
|
||||||
return self._client_params # pragma: no cover
|
return self._client_params
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def experimental(self) -> ExperimentalServerSessionFeatures:
|
def experimental(self) -> ExperimentalServerSessionFeatures:
|
||||||
|
|
@ -126,20 +126,20 @@ class ServerSession(
|
||||||
self._experimental_features = ExperimentalServerSessionFeatures(self)
|
self._experimental_features = ExperimentalServerSessionFeatures(self)
|
||||||
return self._experimental_features
|
return self._experimental_features
|
||||||
|
|
||||||
def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover
|
def check_client_capability(self, capability: types.ClientCapabilities) -> bool:
|
||||||
"""Check if the client supports a specific capability."""
|
"""Check if the client supports a specific capability."""
|
||||||
if self._client_params is None:
|
if self._client_params is None: # pragma: lax no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
client_caps = self._client_params.capabilities
|
client_caps = self._client_params.capabilities
|
||||||
|
|
||||||
if capability.roots is not None:
|
if capability.roots is not None: # pragma: lax no cover
|
||||||
if client_caps.roots is None:
|
if client_caps.roots is None:
|
||||||
return False
|
return False
|
||||||
if capability.roots.list_changed and not client_caps.roots.list_changed:
|
if capability.roots.list_changed and not client_caps.roots.list_changed:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if capability.sampling is not None:
|
if capability.sampling is not None: # pragma: lax no cover
|
||||||
if client_caps.sampling is None:
|
if client_caps.sampling is None:
|
||||||
return False
|
return False
|
||||||
if capability.sampling.context is not None and client_caps.sampling.context is None:
|
if capability.sampling.context is not None and client_caps.sampling.context is None:
|
||||||
|
|
@ -147,17 +147,17 @@ class ServerSession(
|
||||||
if capability.sampling.tools is not None and client_caps.sampling.tools is None:
|
if capability.sampling.tools is not None and client_caps.sampling.tools is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if capability.elicitation is not None and client_caps.elicitation is None:
|
if capability.elicitation is not None and client_caps.elicitation is None: # pragma: lax no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if capability.experimental is not None:
|
if capability.experimental is not None: # pragma: lax no cover
|
||||||
if client_caps.experimental is None:
|
if client_caps.experimental is None:
|
||||||
return False
|
return False
|
||||||
for exp_key, exp_value in capability.experimental.items():
|
for exp_key, exp_value in capability.experimental.items():
|
||||||
if exp_key not in client_caps.experimental or client_caps.experimental[exp_key] != exp_value:
|
if exp_key not in client_caps.experimental or client_caps.experimental[exp_key] != exp_value:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if capability.tasks is not None:
|
if capability.tasks is not None: # pragma: lax no cover
|
||||||
if client_caps.tasks is None:
|
if client_caps.tasks is None:
|
||||||
return False
|
return False
|
||||||
if not check_tasks_capability(capability.tasks, client_caps.tasks):
|
if not check_tasks_capability(capability.tasks, client_caps.tasks):
|
||||||
|
|
@ -544,7 +544,7 @@ class ServerSession(
|
||||||
# Add related-task metadata if associated with a parent task
|
# Add related-task metadata if associated with a parent task
|
||||||
if related_task_id is not None:
|
if related_task_id is not None:
|
||||||
# Defensive: model_dump() never includes _meta, but guard against future changes
|
# Defensive: model_dump() never includes _meta, but guard against future changes
|
||||||
if "_meta" not in params_data: # pragma: no cover
|
if "_meta" not in params_data: # pragma: no branch
|
||||||
params_data["_meta"] = {}
|
params_data["_meta"] = {}
|
||||||
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
||||||
task_id=related_task_id
|
task_id=related_task_id
|
||||||
|
|
@ -589,7 +589,7 @@ class ServerSession(
|
||||||
# Add related-task metadata if associated with a parent task
|
# Add related-task metadata if associated with a parent task
|
||||||
if related_task_id is not None:
|
if related_task_id is not None:
|
||||||
# Defensive: model_dump() never includes _meta, but guard against future changes
|
# Defensive: model_dump() never includes _meta, but guard against future changes
|
||||||
if "_meta" not in params_data: # pragma: no cover
|
if "_meta" not in params_data: # pragma: no branch
|
||||||
params_data["_meta"] = {}
|
params_data["_meta"] = {}
|
||||||
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
||||||
task_id=related_task_id
|
task_id=related_task_id
|
||||||
|
|
@ -659,7 +659,7 @@ class ServerSession(
|
||||||
# Add related-task metadata if associated with a parent task
|
# Add related-task metadata if associated with a parent task
|
||||||
if related_task_id is not None:
|
if related_task_id is not None:
|
||||||
# Defensive: model_dump() never includes _meta, but guard against future changes
|
# Defensive: model_dump() never includes _meta, but guard against future changes
|
||||||
if "_meta" not in params_data: # pragma: no cover
|
if "_meta" not in params_data: # pragma: no branch
|
||||||
params_data["_meta"] = {}
|
params_data["_meta"] = {}
|
||||||
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
||||||
task_id=related_task_id
|
task_id=related_task_id
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue