Add pyright strict mode on the whole project (#1254)
This commit is contained in:
parent
ef4e167c1a
commit
c7671e470c
14 changed files with 652 additions and 674 deletions
|
|
@ -45,7 +45,8 @@ class ResourceServerSettings(BaseSettings):
|
|||
# RFC 8707 resource validation
|
||||
oauth_strict: bool = False
|
||||
|
||||
def __init__(self, **data):
|
||||
# TODO(Marcelo): Is this even needed? I didn't have time to check.
|
||||
def __init__(self, **data: Any):
|
||||
"""Initialize settings with values from environment variables."""
|
||||
super().__init__(**data)
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class SimpleAuthSettings(BaseSettings):
|
|||
mcp_scope: str = "user"
|
||||
|
||||
|
||||
class SimpleOAuthProvider(OAuthAuthorizationServerProvider):
|
||||
class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
|
||||
"""
|
||||
Simple OAuth provider for demo purposes.
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ class SimpleOAuthProvider(OAuthAuthorizationServerProvider):
|
|||
<p>This is a simplified authentication demo. Use the demo credentials below:</p>
|
||||
<p><strong>Username:</strong> demo_user<br>
|
||||
<strong>Password:</strong> demo_password</p>
|
||||
|
||||
|
||||
<form action="{self.server_url.rstrip("/")}/login/callback" method="post">
|
||||
<input type="hidden" name="state" value="{state}">
|
||||
<div class="form-group">
|
||||
|
|
@ -264,7 +264,8 @@ class SimpleOAuthProvider(OAuthAuthorizationServerProvider):
|
|||
"""Exchange refresh token - not supported in this example."""
|
||||
raise NotImplementedError("Refresh tokens not supported")
|
||||
|
||||
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:
|
||||
# TODO(Marcelo): The type hint is wrong. We need to fix, and test to check if it works.
|
||||
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None: # type: ignore
|
||||
"""Revoke a token."""
|
||||
if token in self.tokens:
|
||||
del self.tokens[token]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
||||
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
|
||||
|
|
@ -79,13 +80,13 @@ class IntrospectionTokenVerifier(TokenVerifier):
|
|||
logger.warning(f"Token introspection failed: {e}")
|
||||
return None
|
||||
|
||||
def _validate_resource(self, token_data: dict) -> bool:
|
||||
def _validate_resource(self, token_data: dict[str, Any]) -> bool:
|
||||
"""Validate token was issued for this resource server."""
|
||||
if not self.server_url or not self.resource_url:
|
||||
return False # Fail if strict validation requested but URLs missing
|
||||
|
||||
# Check 'aud' claim first (standard JWT audience)
|
||||
aud = token_data.get("aud")
|
||||
aud: list[str] | str | None = token_data.get("aud")
|
||||
if isinstance(aud, list):
|
||||
for audience in aud:
|
||||
if self._is_valid_resource(audience):
|
||||
|
|
|
|||
|
|
@ -2,22 +2,19 @@ import anyio
|
|||
import click
|
||||
import mcp.types as types
|
||||
from mcp.server.lowlevel import Server
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
def create_messages(
|
||||
context: str | None = None, topic: str | None = None
|
||||
) -> list[types.PromptMessage]:
|
||||
def create_messages(context: str | None = None, topic: str | None = None) -> list[types.PromptMessage]:
|
||||
"""Create the messages for the prompt."""
|
||||
messages = []
|
||||
messages: list[types.PromptMessage] = []
|
||||
|
||||
# Add context if provided
|
||||
if context:
|
||||
messages.append(
|
||||
types.PromptMessage(
|
||||
role="user",
|
||||
content=types.TextContent(
|
||||
type="text", text=f"Here is some relevant context: {context}"
|
||||
),
|
||||
content=types.TextContent(type="text", text=f"Here is some relevant context: {context}"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -28,11 +25,7 @@ def create_messages(
|
|||
else:
|
||||
prompt += "whatever questions I may have."
|
||||
|
||||
messages.append(
|
||||
types.PromptMessage(
|
||||
role="user", content=types.TextContent(type="text", text=prompt)
|
||||
)
|
||||
)
|
||||
messages.append(types.PromptMessage(role="user", content=types.TextContent(type="text", text=prompt)))
|
||||
|
||||
return messages
|
||||
|
||||
|
|
@ -54,8 +47,7 @@ def main(port: int, transport: str) -> int:
|
|||
types.Prompt(
|
||||
name="simple",
|
||||
title="Simple Assistant Prompt",
|
||||
description="A simple prompt that can take optional context and topic "
|
||||
"arguments",
|
||||
description="A simple prompt that can take optional context and topic arguments",
|
||||
arguments=[
|
||||
types.PromptArgument(
|
||||
name="context",
|
||||
|
|
@ -72,9 +64,7 @@ def main(port: int, transport: str) -> int:
|
|||
]
|
||||
|
||||
@app.get_prompt()
|
||||
async def get_prompt(
|
||||
name: str, arguments: dict[str, str] | None = None
|
||||
) -> types.GetPromptResult:
|
||||
async def get_prompt(name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
|
||||
if name != "simple":
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
|
||||
|
|
@ -82,9 +72,7 @@ def main(port: int, transport: str) -> int:
|
|||
arguments = {}
|
||||
|
||||
return types.GetPromptResult(
|
||||
messages=create_messages(
|
||||
context=arguments.get("context"), topic=arguments.get("topic")
|
||||
),
|
||||
messages=create_messages(context=arguments.get("context"), topic=arguments.get("topic")),
|
||||
description="A simple prompt with optional context and topic arguments",
|
||||
)
|
||||
|
||||
|
|
@ -96,13 +84,9 @@ def main(port: int, transport: str) -> int:
|
|||
|
||||
sse = SseServerTransport("/messages/")
|
||||
|
||||
async def handle_sse(request):
|
||||
async with sse.connect_sse(
|
||||
request.scope, request.receive, request._send
|
||||
) as streams:
|
||||
await app.run(
|
||||
streams[0], streams[1], app.create_initialization_options()
|
||||
)
|
||||
async def handle_sse(request: Request):
|
||||
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
|
||||
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||
return Response()
|
||||
|
||||
starlette_app = Starlette(
|
||||
|
|
@ -121,9 +105,7 @@ def main(port: int, transport: str) -> int:
|
|||
|
||||
async def arun():
|
||||
async with stdio_server() as streams:
|
||||
await app.run(
|
||||
streams[0], streams[1], app.create_initialization_options()
|
||||
)
|
||||
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||
|
||||
anyio.run(arun)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ select = ["E", "F", "I"]
|
|||
ignore = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import click
|
|||
import mcp.types as types
|
||||
from mcp.server.lowlevel import Server
|
||||
from pydantic import AnyUrl, FileUrl
|
||||
from starlette.requests import Request
|
||||
|
||||
SAMPLE_RESOURCES = {
|
||||
"greeting": {
|
||||
|
|
@ -63,13 +64,9 @@ def main(port: int, transport: str) -> int:
|
|||
|
||||
sse = SseServerTransport("/messages/")
|
||||
|
||||
async def handle_sse(request):
|
||||
async with sse.connect_sse(
|
||||
request.scope, request.receive, request._send
|
||||
) as streams:
|
||||
await app.run(
|
||||
streams[0], streams[1], app.create_initialization_options()
|
||||
)
|
||||
async def handle_sse(request: Request):
|
||||
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
|
||||
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||
return Response()
|
||||
|
||||
starlette_app = Starlette(
|
||||
|
|
@ -88,9 +85,7 @@ def main(port: int, transport: str) -> int:
|
|||
|
||||
async def arun():
|
||||
async with stdio_server() as streams:
|
||||
await app.run(
|
||||
streams[0], streams[1], app.create_initialization_options()
|
||||
)
|
||||
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||
|
||||
anyio.run(arun)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ select = ["E", "F", "I"]
|
|||
ignore = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import contextlib
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import click
|
||||
|
|
@ -41,7 +42,7 @@ def main(
|
|||
app = Server("mcp-streamable-http-stateless-demo")
|
||||
|
||||
@app.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
|
||||
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
|
||||
ctx = app.request_context
|
||||
interval = arguments.get("interval", 1.0)
|
||||
count = arguments.get("count", 5)
|
||||
|
|
@ -61,10 +62,7 @@ def main(
|
|||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=(
|
||||
f"Sent {count} notifications with {interval}s interval"
|
||||
f" for caller: {caller}"
|
||||
),
|
||||
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -73,10 +71,7 @@ def main(
|
|||
return [
|
||||
types.Tool(
|
||||
name="start-notification-stream",
|
||||
description=(
|
||||
"Sends a stream of notifications with configurable count"
|
||||
" and interval"
|
||||
),
|
||||
description=("Sends a stream of notifications with configurable count and interval"),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"required": ["interval", "count", "caller"],
|
||||
|
|
@ -91,9 +86,7 @@ def main(
|
|||
},
|
||||
"caller": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Identifier of the caller to include in notifications"
|
||||
),
|
||||
"description": ("Identifier of the caller to include in notifications"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -108,9 +101,7 @@ def main(
|
|||
stateless=True,
|
||||
)
|
||||
|
||||
async def handle_streamable_http(
|
||||
scope: Scope, receive: Receive, send: Send
|
||||
) -> None:
|
||||
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
||||
await session_manager.handle_request(scope, receive, send)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ select = ["E", "F", "I"]
|
|||
ignore = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
|
||||
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
|
||||
|
|
|
|||
|
|
@ -10,13 +10,7 @@ from collections import deque
|
|||
from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
from mcp.server.streamable_http import (
|
||||
EventCallback,
|
||||
EventId,
|
||||
EventMessage,
|
||||
EventStore,
|
||||
StreamId,
|
||||
)
|
||||
from mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId
|
||||
from mcp.types import JSONRPCMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -54,14 +48,10 @@ class InMemoryEventStore(EventStore):
|
|||
# event_id -> EventEntry for quick lookup
|
||||
self.event_index: dict[EventId, EventEntry] = {}
|
||||
|
||||
async def store_event(
|
||||
self, stream_id: StreamId, message: JSONRPCMessage
|
||||
) -> EventId:
|
||||
async def store_event(self, stream_id: StreamId, message: JSONRPCMessage) -> EventId:
|
||||
"""Stores an event with a generated event ID."""
|
||||
event_id = str(uuid4())
|
||||
event_entry = EventEntry(
|
||||
event_id=event_id, stream_id=stream_id, message=message
|
||||
)
|
||||
event_entry = EventEntry(event_id=event_id, stream_id=stream_id, message=message)
|
||||
|
||||
# Get or create deque for this stream
|
||||
if stream_id not in self.streams:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import contextlib
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import click
|
||||
|
|
@ -45,7 +46,7 @@ def main(
|
|||
app = Server("mcp-streamable-http-demo")
|
||||
|
||||
@app.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
|
||||
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
|
||||
ctx = app.request_context
|
||||
interval = arguments.get("interval", 1.0)
|
||||
count = arguments.get("count", 5)
|
||||
|
|
@ -54,10 +55,7 @@ def main(
|
|||
# Send the specified number of notifications with the given interval
|
||||
for i in range(count):
|
||||
# Include more detailed message for resumability demonstration
|
||||
notification_msg = (
|
||||
f"[{i + 1}/{count}] Event from '{caller}' - "
|
||||
f"Use Last-Event-ID to resume if disconnected"
|
||||
)
|
||||
notification_msg = f"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected"
|
||||
await ctx.session.send_log_message(
|
||||
level="info",
|
||||
data=notification_msg,
|
||||
|
|
@ -79,10 +77,7 @@ def main(
|
|||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=(
|
||||
f"Sent {count} notifications with {interval}s interval"
|
||||
f" for caller: {caller}"
|
||||
),
|
||||
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -91,10 +86,7 @@ def main(
|
|||
return [
|
||||
types.Tool(
|
||||
name="start-notification-stream",
|
||||
description=(
|
||||
"Sends a stream of notifications with configurable count"
|
||||
" and interval"
|
||||
),
|
||||
description=("Sends a stream of notifications with configurable count and interval"),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"required": ["interval", "count", "caller"],
|
||||
|
|
@ -109,9 +101,7 @@ def main(
|
|||
},
|
||||
"caller": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Identifier of the caller to include in notifications"
|
||||
),
|
||||
"description": ("Identifier of the caller to include in notifications"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -136,9 +126,7 @@ def main(
|
|||
)
|
||||
|
||||
# ASGI handler for streamable HTTP connections
|
||||
async def handle_streamable_http(
|
||||
scope: Scope, receive: Receive, send: Send
|
||||
) -> None:
|
||||
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
||||
await session_manager.handle_request(scope, receive, send)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ select = ["E", "F", "I"]
|
|||
ignore = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
|
||||
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import click
|
||||
import mcp.types as types
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.shared._httpx_utils import create_mcp_http_client
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
async def fetch_website(
|
||||
url: str,
|
||||
) -> list[types.ContentBlock]:
|
||||
headers = {
|
||||
"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
|
||||
}
|
||||
headers = {"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"}
|
||||
async with create_mcp_http_client(headers=headers) as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
|
|
@ -29,7 +30,7 @@ def main(port: int, transport: str) -> int:
|
|||
app = Server("mcp-website-fetcher")
|
||||
|
||||
@app.call_tool()
|
||||
async def fetch_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
|
||||
async def fetch_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
|
||||
if name != "fetch":
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
if "url" not in arguments:
|
||||
|
|
@ -64,13 +65,9 @@ def main(port: int, transport: str) -> int:
|
|||
|
||||
sse = SseServerTransport("/messages/")
|
||||
|
||||
async def handle_sse(request):
|
||||
async with sse.connect_sse(
|
||||
request.scope, request.receive, request._send
|
||||
) as streams:
|
||||
await app.run(
|
||||
streams[0], streams[1], app.create_initialization_options()
|
||||
)
|
||||
async def handle_sse(request: Request):
|
||||
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
|
||||
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||
return Response()
|
||||
|
||||
starlette_app = Starlette(
|
||||
|
|
@ -89,9 +86,7 @@ def main(port: int, transport: str) -> int:
|
|||
|
||||
async def arun():
|
||||
async with stdio_server() as streams:
|
||||
await app.run(
|
||||
streams[0], streams[1], app.create_initialization_options()
|
||||
)
|
||||
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||
|
||||
anyio.run(arun)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ select = ["E", "F", "I"]
|
|||
ignore = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
Loading…
Reference in a new issue