Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add strict mode to pyright #315

Merged
merged 12 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 32 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,36 @@
<!-- omit in toc -->
## Table of Contents

- [Overview](#overview)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My IDE "fixes" this... It has been like this for years and I never bothered to fix it... If it's not important, I'd appreciate it if we just ignore this.

- [Installation](#installation)
- [Quickstart](#quickstart)
- [What is MCP?](#what-is-mcp)
- [Core Concepts](#core-concepts)
- [Server](#server)
- [Resources](#resources)
- [Tools](#tools)
- [Prompts](#prompts)
- [Images](#images)
- [Context](#context)
- [Running Your Server](#running-your-server)
- [Development Mode](#development-mode)
- [Claude Desktop Integration](#claude-desktop-integration)
- [Direct Execution](#direct-execution)
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
- [Examples](#examples)
- [Echo Server](#echo-server)
- [SQLite Explorer](#sqlite-explorer)
- [Advanced Usage](#advanced-usage)
- [Low-Level Server](#low-level-server)
- [Writing MCP Clients](#writing-mcp-clients)
- [MCP Primitives](#mcp-primitives)
- [Server Capabilities](#server-capabilities)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
- [MCP Python SDK](#mcp-python-sdk)
- [Overview](#overview)
- [Installation](#installation)
- [Adding MCP to your python project](#adding-mcp-to-your-python-project)
- [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools)
- [Quickstart](#quickstart)
- [What is MCP?](#what-is-mcp)
- [Core Concepts](#core-concepts)
- [Server](#server)
- [Resources](#resources)
- [Tools](#tools)
- [Prompts](#prompts)
- [Images](#images)
- [Context](#context)
- [Running Your Server](#running-your-server)
- [Development Mode](#development-mode)
- [Claude Desktop Integration](#claude-desktop-integration)
- [Direct Execution](#direct-execution)
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
- [Examples](#examples)
- [Echo Server](#echo-server)
- [SQLite Explorer](#sqlite-explorer)
- [Advanced Usage](#advanced-usage)
- [Low-Level Server](#low-level-server)
- [Writing MCP Clients](#writing-mcp-clients)
- [MCP Primitives](#mcp-primitives)
- [Server Capabilities](#server-capabilities)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)

[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg
[pypi-url]: https://pypi.org/project/mcp/
Expand Down Expand Up @@ -143,8 +146,8 @@ The FastMCP server is your core interface to the MCP protocol. It handles connec
```python
# Add lifespan support for startup/shutdown with strong typing
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import AsyncIterator

from fake_database import Database # Replace with your actual DB type

Expand Down Expand Up @@ -442,7 +445,7 @@ For more control, you can use the low-level server implementation directly. This

```python
from contextlib import asynccontextmanager
from typing import AsyncIterator
from collections.abc import AsyncIterator

from fake_database import Database # Replace with your actual DB type

Expand Down
8 changes: 3 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,11 @@ packages = ["src/mcp"]
include = ["src/mcp", "tests"]
venvPath = "."
venv = ".venv"
strict = [
"src/mcp/server/fastmcp/tools/base.py",
"src/mcp/client/*.py"
]
strict = ["src/mcp/**/*.py"]
exclude = ["src/mcp/types.py"]

[tool.ruff.lint]
select = ["E", "F", "I"]
select = ["E", "F", "I", "UP"]
ignore = []

[tool.ruff]
Expand Down
6 changes: 2 additions & 4 deletions src/mcp/cli/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import sys
from pathlib import Path
from typing import Any

from mcp.server.fastmcp.utilities.logging import get_logger

Expand Down Expand Up @@ -116,10 +117,7 @@ def update_claude_config(
# Add fastmcp run command
args.extend(["mcp", "run", file_spec])

server_config = {
"command": "uv",
"args": args,
}
server_config: dict[str, Any] = {"command": "uv", "args": args}

# Add environment variables if specified
if env_vars:
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/client/websocket.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import AsyncGenerator

import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
Expand Down
38 changes: 19 additions & 19 deletions src/mcp/server/fastmcp/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import inspect
import json
from collections.abc import Callable
from typing import Any, Awaitable, Literal, Sequence
from collections.abc import Awaitable, Callable, Sequence
from typing import Any, Literal

import pydantic_core
from pydantic import BaseModel, Field, TypeAdapter, validate_call
Expand All @@ -19,7 +19,7 @@ class Message(BaseModel):
role: Literal["user", "assistant"]
content: CONTENT_TYPES

def __init__(self, content: str | CONTENT_TYPES, **kwargs):
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
if isinstance(content, str):
content = TextContent(type="text", text=content)
super().__init__(content=content, **kwargs)
Expand All @@ -30,7 +30,7 @@ class UserMessage(Message):

role: Literal["user", "assistant"] = "user"

def __init__(self, content: str | CONTENT_TYPES, **kwargs):
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
super().__init__(content=content, **kwargs)


Expand All @@ -39,11 +39,13 @@ class AssistantMessage(Message):

role: Literal["user", "assistant"] = "assistant"

def __init__(self, content: str | CONTENT_TYPES, **kwargs):
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
super().__init__(content=content, **kwargs)


message_validator = TypeAdapter(UserMessage | AssistantMessage)
message_validator = TypeAdapter[UserMessage | AssistantMessage](
UserMessage | AssistantMessage
)

SyncPromptResult = (
str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
Expand Down Expand Up @@ -73,12 +75,12 @@ class Prompt(BaseModel):
arguments: list[PromptArgument] | None = Field(
None, description="Arguments that can be passed to the prompt"
)
fn: Callable = Field(exclude=True)
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)

@classmethod
def from_function(
cls,
fn: Callable[..., PromptResult],
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
name: str | None = None,
description: str | None = None,
) -> "Prompt":
Expand All @@ -99,7 +101,7 @@ def from_function(
parameters = TypeAdapter(fn).json_schema()

# Convert parameters to PromptArguments
arguments = []
arguments: list[PromptArgument] = []
if "properties" in parameters:
for param_name, param in parameters["properties"].items():
required = param_name in parameters.get("required", [])
Expand Down Expand Up @@ -138,25 +140,23 @@ async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]
result = await result

# Validate messages
if not isinstance(result, (list, tuple)):
if not isinstance(result, list | tuple):
result = [result]

# Convert result to messages
messages = []
for msg in result:
messages: list[Message] = []
for msg in result: # type: ignore[reportUnknownVariableType]
try:
if isinstance(msg, Message):
messages.append(msg)
elif isinstance(msg, dict):
msg = message_validator.validate_python(msg)
messages.append(msg)
messages.append(message_validator.validate_python(msg))
elif isinstance(msg, str):
messages.append(
UserMessage(content=TextContent(type="text", text=msg))
)
content = TextContent(type="text", text=msg)
messages.append(UserMessage(content=content))
else:
msg = json.dumps(pydantic_core.to_jsonable_python(msg))
messages.append(Message(role="user", content=msg))
content = json.dumps(pydantic_core.to_jsonable_python(msg))
messages.append(Message(role="user", content=content))
except Exception:
raise ValueError(
f"Could not convert prompt result to message: {msg}"
Expand Down
5 changes: 3 additions & 2 deletions src/mcp/server/fastmcp/resources/resource_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Resource manager functionality."""

from typing import Callable
from collections.abc import Callable
from typing import Any

from pydantic import AnyUrl

Expand Down Expand Up @@ -47,7 +48,7 @@ def add_resource(self, resource: Resource) -> Resource:

def add_template(
self,
fn: Callable,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
description: str | None = None,
Expand Down
15 changes: 10 additions & 5 deletions src/mcp/server/fastmcp/resources/templates.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Resource template functionality."""

from __future__ import annotations

import inspect
import re
from typing import Any, Callable
from collections.abc import Callable
from typing import Any

from pydantic import BaseModel, Field, TypeAdapter, validate_call

Expand All @@ -20,18 +23,20 @@ class ResourceTemplate(BaseModel):
mime_type: str = Field(
default="text/plain", description="MIME type of the resource content"
)
fn: Callable = Field(exclude=True)
parameters: dict = Field(description="JSON schema for function parameters")
fn: Callable[..., Any] = Field(exclude=True)
parameters: dict[str, Any] = Field(
description="JSON schema for function parameters"
)

@classmethod
def from_function(
cls,
fn: Callable,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> "ResourceTemplate":
) -> ResourceTemplate:
"""Create a template from a function."""
func_name = name or fn.__name__
if func_name == "<lambda>":
Expand Down
23 changes: 13 additions & 10 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import inspect
import json
import re
from collections.abc import AsyncIterator, Iterable
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
from contextlib import (
AbstractAsyncContextManager,
asynccontextmanager,
)
from itertools import chain
from typing import Any, Callable, Generic, Literal, Sequence
from typing import Any, Generic, Literal

import anyio
import pydantic_core
Expand All @@ -20,6 +20,7 @@
from pydantic.networks import AnyUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.routing import Mount, Route

from mcp.server.fastmcp.exceptions import ResourceError
Expand Down Expand Up @@ -88,13 +89,13 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
)

lifespan: (
Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]] | None
) = Field(None, description="Lifespan context manager")


def lifespan_wrapper(
app: FastMCP,
lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
) -> Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[object]]:
@asynccontextmanager
async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[object]:
Expand Down Expand Up @@ -179,7 +180,7 @@ async def list_tools(self) -> list[MCPTool]:
for info in tools
]

def get_context(self) -> "Context[ServerSession, object]":
def get_context(self) -> Context[ServerSession, object]:
"""
Returns a Context object. Note that the context will only be valid
during a request; outside a request, most methods will error.
Expand Down Expand Up @@ -478,9 +479,11 @@ def sse_app(self) -> Starlette:
"""Return an instance of the SSE server app."""
sse = SseServerTransport("/messages/")

async def handle_sse(request):
async def handle_sse(request: Request) -> None:
async with sse.connect_sse(
request.scope, request.receive, request._send
request.scope,
request.receive,
request._send, # type: ignore[reportPrivateUsage]
) as streams:
await self._mcp_server.run(
streams[0],
Expand Down Expand Up @@ -535,14 +538,14 @@ def _convert_to_content(
if result is None:
return []

if isinstance(result, (TextContent, ImageContent, EmbeddedResource)):
if isinstance(result, TextContent | ImageContent | EmbeddedResource):
return [result]

if isinstance(result, Image):
return [result.to_image_content()]

if isinstance(result, (list, tuple)):
return list(chain.from_iterable(_convert_to_content(item) for item in result))
if isinstance(result, list | tuple):
return list(chain.from_iterable(_convert_to_content(item) for item in result)) # type: ignore[reportUnknownVariableType]

if not isinstance(result, str):
try:
Expand Down
Loading