Skip to content

Commit c7671e4

Browse files
authored
Add pyright strict mode on the whole project (#1254)
1 parent ef4e167 commit c7671e4

File tree

75 files changed

+652
-674
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+652
-674
lines changed

.github/workflows/shared.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
permissions:
77
contents: read
88

9+
env:
10+
COLUMNS: 150
11+
912
jobs:
1013
pre-commit:
1114
runs-on: ubuntu-latest

README.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ from contextlib import asynccontextmanager
197197
from dataclasses import dataclass
198198

199199
from mcp.server.fastmcp import Context, FastMCP
200+
from mcp.server.session import ServerSession
200201

201202

202203
# Mock database class for example
@@ -242,7 +243,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
242243

243244
# Access type-safe lifespan context in tools
244245
@mcp.tool()
245-
def query_db(ctx: Context) -> str:
246+
def query_db(ctx: Context[ServerSession, AppContext]) -> str:
246247
"""Tool that uses initialized resources."""
247248
db = ctx.request_context.lifespan_context.db
248249
return db.query()
@@ -314,12 +315,13 @@ Tools can optionally receive a Context object by including a parameter with the
314315
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
315316
```python
316317
from mcp.server.fastmcp import Context, FastMCP
318+
from mcp.server.session import ServerSession
317319

318320
mcp = FastMCP(name="Progress Example")
319321

320322

321323
@mcp.tool()
322-
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
324+
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
323325
"""Execute a task with progress updates."""
324326
await ctx.info(f"Starting: {task_name}")
325327

@@ -445,7 +447,7 @@ def get_user(user_id: str) -> UserProfile:
445447

446448
# Classes WITHOUT type hints cannot be used for structured output
447449
class UntypedConfig:
448-
def __init__(self, setting1, setting2):
450+
def __init__(self, setting1, setting2): # type: ignore[reportMissingParameterType]
449451
self.setting1 = setting1
450452
self.setting2 = setting2
451453

@@ -571,12 +573,13 @@ The Context object provides the following capabilities:
571573
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
572574
```python
573575
from mcp.server.fastmcp import Context, FastMCP
576+
from mcp.server.session import ServerSession
574577

575578
mcp = FastMCP(name="Progress Example")
576579

577580

578581
@mcp.tool()
579-
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
582+
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
580583
"""Execute a task with progress updates."""
581584
await ctx.info(f"Starting: {task_name}")
582585

@@ -694,6 +697,7 @@ Request additional information from users. This example shows an Elicitation dur
694697
from pydantic import BaseModel, Field
695698

696699
from mcp.server.fastmcp import Context, FastMCP
700+
from mcp.server.session import ServerSession
697701

698702
mcp = FastMCP(name="Elicitation Example")
699703

@@ -709,12 +713,7 @@ class BookingPreferences(BaseModel):
709713

710714

711715
@mcp.tool()
712-
async def book_table(
713-
date: str,
714-
time: str,
715-
party_size: int,
716-
ctx: Context,
717-
) -> str:
716+
async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:
718717
"""Book a table with date availability check."""
719718
# Check if date is available
720719
if date == "2024-12-25":
@@ -750,13 +749,14 @@ Tools can interact with LLMs through sampling (generating text):
750749
<!-- snippet-source examples/snippets/servers/sampling.py -->
751750
```python
752751
from mcp.server.fastmcp import Context, FastMCP
752+
from mcp.server.session import ServerSession
753753
from mcp.types import SamplingMessage, TextContent
754754

755755
mcp = FastMCP(name="Sampling Example")
756756

757757

758758
@mcp.tool()
759-
async def generate_poem(topic: str, ctx: Context) -> str:
759+
async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
760760
"""Generate a poem using LLM sampling."""
761761
prompt = f"Write a short poem about {topic}"
762762

@@ -785,12 +785,13 @@ Tools can send logs and notifications through the context:
785785
<!-- snippet-source examples/snippets/servers/notifications.py -->
786786
```python
787787
from mcp.server.fastmcp import Context, FastMCP
788+
from mcp.server.session import ServerSession
788789

789790
mcp = FastMCP(name="Notifications Example")
790791

791792

792793
@mcp.tool()
793-
async def process_data(data: str, ctx: Context) -> str:
794+
async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
794795
"""Process data with logging."""
795796
# Different log levels
796797
await ctx.debug(f"Debug: Processing '{data}'")
@@ -1244,6 +1245,7 @@ Run from the repository root:
12441245

12451246
from collections.abc import AsyncIterator
12461247
from contextlib import asynccontextmanager
1248+
from typing import Any
12471249

12481250
import mcp.server.stdio
12491251
import mcp.types as types
@@ -1272,7 +1274,7 @@ class Database:
12721274

12731275

12741276
@asynccontextmanager
1275-
async def server_lifespan(_server: Server) -> AsyncIterator[dict]:
1277+
async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]:
12761278
"""Manage server startup and shutdown lifecycle."""
12771279
# Initialize resources on startup
12781280
db = await Database.connect()
@@ -1304,7 +1306,7 @@ async def handle_list_tools() -> list[types.Tool]:
13041306

13051307

13061308
@server.call_tool()
1307-
async def query_db(name: str, arguments: dict) -> list[types.TextContent]:
1309+
async def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
13081310
"""Handle database query tool call."""
13091311
if name != "query_db":
13101312
raise ValueError(f"Unknown tool: {name}")
@@ -1558,7 +1560,7 @@ server_params = StdioServerParameters(
15581560

15591561
# Optional: create a sampling callback
15601562
async def handle_sampling_message(
1561-
context: RequestContext, params: types.CreateMessageRequestParams
1563+
context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams
15621564
) -> types.CreateMessageResult:
15631565
print(f"Sampling request: {params.messages}")
15641566
return types.CreateMessageResult(

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
188188
# Create OAuth authentication handler using the new interface
189189
oauth_auth = OAuthClientProvider(
190190
server_url=self.server_url.replace("/mcp", ""),
191-
client_metadata=OAuthClientMetadata.model_validate(
192-
client_metadata_dict
193-
),
191+
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
194192
storage=InMemoryTokenStorage(),
195193
redirect_handler=_default_redirect_handler,
196194
callback_handler=callback_handler,
@@ -322,9 +320,7 @@ async def interactive_loop(self):
322320
await self.call_tool(tool_name, arguments)
323321

324322
else:
325-
print(
326-
"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
327-
)
323+
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")
328324

329325
except KeyboardInterrupt:
330326
print("\n\n👋 Goodbye!")

examples/clients/simple-auth-client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ select = ["E", "F", "I"]
3939
ignore = []
4040

4141
[tool.ruff]
42-
line-length = 88
42+
line-length = 120
4343
target-version = "py310"
4444

4545
[tool.uv]

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
from mcp.client.stdio import stdio_client
1313

1414
# Configure logging
15-
logging.basicConfig(
16-
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
17-
)
15+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
1816

1917

2018
class Configuration:
@@ -75,29 +73,19 @@ def __init__(self, name: str, config: dict[str, Any]) -> None:
7573

7674
async def initialize(self) -> None:
7775
"""Initialize the server connection."""
78-
command = (
79-
shutil.which("npx")
80-
if self.config["command"] == "npx"
81-
else self.config["command"]
82-
)
76+
command = shutil.which("npx") if self.config["command"] == "npx" else self.config["command"]
8377
if command is None:
8478
raise ValueError("The command must be a valid string and cannot be None.")
8579

8680
server_params = StdioServerParameters(
8781
command=command,
8882
args=self.config["args"],
89-
env={**os.environ, **self.config["env"]}
90-
if self.config.get("env")
91-
else None,
83+
env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
9284
)
9385
try:
94-
stdio_transport = await self.exit_stack.enter_async_context(
95-
stdio_client(server_params)
96-
)
86+
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
9787
read, write = stdio_transport
98-
session = await self.exit_stack.enter_async_context(
99-
ClientSession(read, write)
100-
)
88+
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
10189
await session.initialize()
10290
self.session = session
10391
except Exception as e:
@@ -122,10 +110,7 @@ async def list_tools(self) -> list[Any]:
122110

123111
for item in tools_response:
124112
if isinstance(item, tuple) and item[0] == "tools":
125-
tools.extend(
126-
Tool(tool.name, tool.description, tool.inputSchema, tool.title)
127-
for tool in item[1]
128-
)
113+
tools.extend(Tool(tool.name, tool.description, tool.inputSchema, tool.title) for tool in item[1])
129114

130115
return tools
131116

@@ -164,9 +149,7 @@ async def execute_tool(
164149

165150
except Exception as e:
166151
attempt += 1
167-
logging.warning(
168-
f"Error executing tool: {e}. Attempt {attempt} of {retries}."
169-
)
152+
logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.")
170153
if attempt < retries:
171154
logging.info(f"Retrying in {delay} seconds...")
172155
await asyncio.sleep(delay)
@@ -209,9 +192,7 @@ def format_for_llm(self) -> str:
209192
args_desc = []
210193
if "properties" in self.input_schema:
211194
for param_name, param_info in self.input_schema["properties"].items():
212-
arg_desc = (
213-
f"- {param_name}: {param_info.get('description', 'No description')}"
214-
)
195+
arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}"
215196
if param_name in self.input_schema.get("required", []):
216197
arg_desc += " (required)"
217198
args_desc.append(arg_desc)
@@ -281,10 +262,7 @@ def get_response(self, messages: list[dict[str, str]]) -> str:
281262
logging.error(f"Status code: {status_code}")
282263
logging.error(f"Response details: {e.response.text}")
283264

284-
return (
285-
f"I encountered an error: {error_message}. "
286-
"Please try again or rephrase your request."
287-
)
265+
return f"I encountered an error: {error_message}. Please try again or rephrase your request."
288266

289267

290268
class ChatSession:
@@ -323,17 +301,13 @@ async def process_llm_response(self, llm_response: str) -> str:
323301
tools = await server.list_tools()
324302
if any(tool.name == tool_call["tool"] for tool in tools):
325303
try:
326-
result = await server.execute_tool(
327-
tool_call["tool"], tool_call["arguments"]
328-
)
304+
result = await server.execute_tool(tool_call["tool"], tool_call["arguments"])
329305

330306
if isinstance(result, dict) and "progress" in result:
331307
progress = result["progress"]
332308
total = result["total"]
333309
percentage = (progress / total) * 100
334-
logging.info(
335-
f"Progress: {progress}/{total} ({percentage:.1f}%)"
336-
)
310+
logging.info(f"Progress: {progress}/{total} ({percentage:.1f}%)")
337311

338312
return f"Tool execution result: {result}"
339313
except Exception as e:
@@ -408,9 +382,7 @@ async def start(self) -> None:
408382

409383
final_response = self.llm_client.get_response(messages)
410384
logging.info("\nFinal response: %s", final_response)
411-
messages.append(
412-
{"role": "assistant", "content": final_response}
413-
)
385+
messages.append({"role": "assistant", "content": final_response})
414386
else:
415387
messages.append({"role": "assistant", "content": llm_response})
416388

@@ -426,10 +398,7 @@ async def main() -> None:
426398
"""Initialize and run the chat session."""
427399
config = Configuration()
428400
server_config = config.load_config("servers_config.json")
429-
servers = [
430-
Server(name, srv_config)
431-
for name, srv_config in server_config["mcpServers"].items()
432-
]
401+
servers = [Server(name, srv_config) for name, srv_config in server_config["mcpServers"].items()]
433402
llm_client = LLMClient(config.llm_api_key)
434403
chat_session = ChatSession(servers, llm_client)
435404
await chat_session.start()

examples/clients/simple-chatbot/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ select = ["E", "F", "I"]
4141
ignore = []
4242

4343
[tool.ruff]
44-
line-length = 88
44+
line-length = 120
4545
target-version = "py310"
4646

4747
[tool.uv]

examples/servers/simple-auth/mcp_simple_auth/server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ class ResourceServerSettings(BaseSettings):
4545
# RFC 8707 resource validation
4646
oauth_strict: bool = False
4747

48-
def __init__(self, **data):
48+
# TODO(Marcelo): Is this even needed? I didn't have time to check.
49+
def __init__(self, **data: Any):
4950
"""Initialize settings with values from environment variables."""
5051
super().__init__(**data)
5152

examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class SimpleAuthSettings(BaseSettings):
4646
mcp_scope: str = "user"
4747

4848

49-
class SimpleOAuthProvider(OAuthAuthorizationServerProvider):
49+
class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
5050
"""
5151
Simple OAuth provider for demo purposes.
5252
@@ -116,7 +116,7 @@ async def get_login_page(self, state: str) -> HTMLResponse:
116116
<p>This is a simplified authentication demo. Use the demo credentials below:</p>
117117
<p><strong>Username:</strong> demo_user<br>
118118
<strong>Password:</strong> demo_password</p>
119-
119+
120120
<form action="{self.server_url.rstrip("/")}/login/callback" method="post">
121121
<input type="hidden" name="state" value="{state}">
122122
<div class="form-group">
@@ -264,7 +264,8 @@ async def exchange_refresh_token(
264264
"""Exchange refresh token - not supported in this example."""
265265
raise NotImplementedError("Refresh tokens not supported")
266266

267-
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:
267+
# TODO(Marcelo): The type hint is wrong. We need to fix, and test to check if it works.
268+
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None: # type: ignore
268269
"""Revoke a token."""
269270
if token in self.tokens:
270271
del self.tokens[token]

examples/servers/simple-auth/mcp_simple_auth/token_verifier.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
22

33
import logging
4+
from typing import Any
45

56
from mcp.server.auth.provider import AccessToken, TokenVerifier
67
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
@@ -79,13 +80,13 @@ async def verify_token(self, token: str) -> AccessToken | None:
7980
logger.warning(f"Token introspection failed: {e}")
8081
return None
8182

82-
def _validate_resource(self, token_data: dict) -> bool:
83+
def _validate_resource(self, token_data: dict[str, Any]) -> bool:
8384
"""Validate token was issued for this resource server."""
8485
if not self.server_url or not self.resource_url:
8586
return False # Fail if strict validation requested but URLs missing
8687

8788
# Check 'aud' claim first (standard JWT audience)
88-
aud = token_data.get("aud")
89+
aud: list[str] | str | None = token_data.get("aud")
8990
if isinstance(aud, list):
9091
for audience in aud:
9192
if self._is_valid_resource(audience):

0 commit comments

Comments
 (0)