diff --git a/README.md b/README.md index e2da55c36..97b42c99e 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ For development, install with: git clone https://github.com/jlowin/fastmcp.git cd fastmcp # Install with dev dependencies -uv sync --dev +uv sync ``` ## Quickstart @@ -589,7 +589,7 @@ Contributions make the open-source community vibrant! We welcome improvements an #### Setup 1. Clone: `git clone https://github.com/jlowin/fastmcp.git && cd fastmcp` -2. Install Env & Dependencies: `uv venv && uv sync --dev` (Activate the `.venv` after creation) +2. Install Env & Dependencies: `uv venv && uv sync` (Activate the `.venv` after creation) #### Testing diff --git a/examples/mount_example.py b/examples/mount_example.py index fab26aa1e..73661f0d2 100644 --- a/examples/mount_example.py +++ b/examples/mount_example.py @@ -9,7 +9,6 @@ the ToolManager's import_tools functionality. It shows how to: """ import asyncio -from typing import Dict, List from fastmcp import FastMCP @@ -34,7 +33,7 @@ news_app = FastMCP("News App") @news_app.tool() -def get_news_headlines() -> List[str]: +def get_news_headlines() -> list[str]: """Get the latest news headlines.""" return [ "Tech company launches new product", @@ -58,7 +57,7 @@ app = FastMCP("Main App") @app.tool() -def check_app_status() -> Dict[str, str]: +def check_app_status() -> dict[str, str]: """Check the status of the main application.""" return {"status": "running", "version": "1.0.0", "uptime": "3h 24m"} diff --git a/pyproject.toml b/pyproject.toml index 26720e1f1..8dcc4ab38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,12 +16,6 @@ requires-python = ">=3.10" readme = "README.md" license = { text = "Apache-2.0" } -[project.scripts] -fastmcp = "fastmcp.cli:app" - -[project.optional-dependencies] - - [dependency-groups] dev = [ "pre-commit", @@ -36,23 +30,32 @@ dev = [ "pdbpp>=0.10.3", "dirty-equals>=0.9.0", ] + +[project.scripts] +fastmcp = "fastmcp.cli:app" + [build-system] -requires = ["hatchling>=1.21.0", "hatch-vcs>=0.4.0"] +requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +vcs = "git" +style = "pep440" +bump = true +fallback-version = "0.0.0" + [tool.uv] -# no default groups -default-groups = [] +# uncomment to omit `dev` default group +# default-groups = [] [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" -filterwarnings = [ - "ignore:Accessing the 'model_fields' attribute on the instance is deprecated:DeprecationWarning", -] +filterwarnings = [] -[tool.hatch.version] -source = "vcs" [tool.pyright] include = ["src", "tests"] @@ -67,7 +70,7 @@ venvPath = "." venv = ".venv" [tool.ruff.lint] -extend-select = ["I"] +extend-select = ["I", "UP"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "I001", "RUF013"] diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 37c924b5c..c6d2a7cdd 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -1,6 +1,7 @@ import datetime +from contextlib import AbstractAsyncContextManager from pathlib import Path -from typing import Any, AsyncContextManager +from typing import Any import mcp.types from mcp import ClientSession @@ -48,7 +49,7 @@ class Client: ): self.transport = infer_transport(transport) self._session: ClientSession | None = None - self._session_cm: AsyncContextManager[ClientSession] | None = None + self._session_cm: AbstractAsyncContextManager[ClientSession] | None = None # Store common kwargs to pass to transport.connect_session if roots is not None and list_roots_callback is not None: diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 3a81f2ff3..68eae48d3 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -2,14 +2,10 @@ import abc import contextlib import datetime import os +from collections.abc import AsyncIterator from pathlib import Path from typing import ( - AsyncIterator, - Dict, - List, - Optional, TypedDict, - Union, ) from mcp import ClientSession, StdioServerParameters @@ -103,7 +99,7 @@ class WSTransport(ClientTransport): class SSETransport(ClientTransport): """Transport implementation that connects to an MCP server via Server-Sent Events.""" - def __init__(self, url: str | AnyUrl, headers: Optional[Dict[str, str]] = None): + def __init__(self, url: str | AnyUrl, headers: dict[str, str] | None = None): if isinstance(url, AnyUrl): url = str(url) if not isinstance(url, str) or not url.startswith("http"): @@ -138,9 +134,9 @@ class StdioTransport(ClientTransport): def __init__( self, command: str, - args: List[str], - env: Optional[Dict[str, str]] = None, - cwd: Optional[str] = None, + args: list[str], + env: dict[str, str] | None = None, + cwd: str | None = None, ): """ Initialize a Stdio transport. @@ -182,10 +178,10 @@ class PythonStdioTransport(StdioTransport): def __init__( self, - script_path: Union[str, Path], - args: Optional[List[str]] = None, - env: Optional[Dict[str, str]] = None, - cwd: Optional[str] = None, + script_path: str | Path, + args: list[str] | None = None, + env: dict[str, str] | None = None, + cwd: str | None = None, python_cmd: str = "python", ): """ @@ -217,10 +213,10 @@ class NodeStdioTransport(StdioTransport): def __init__( self, - script_path: Union[str, Path], - args: Optional[List[str]] = None, - env: Optional[Dict[str, str]] = None, - cwd: Optional[str] = None, + script_path: str | Path, + args: list[str] | None = None, + env: dict[str, str] | None = None, + cwd: str | None = None, node_cmd: str = "node", ): """ @@ -253,12 +249,12 @@ class UvxStdioTransport(StdioTransport): def __init__( self, tool_name: str, - tool_args: Optional[List[str]] = None, - project_directory: Optional[str] = None, - python_version: Optional[str] = None, - with_packages: Optional[List[str]] = None, - from_package: Optional[str] = None, - env_vars: Optional[Dict[str, str]] = None, + tool_args: list[str] | None = None, + project_directory: str | None = None, + python_version: str | None = None, + with_packages: list[str] | None = None, + from_package: str | None = None, + env_vars: dict[str, str] | None = None, ): """ Initialize a Uvx transport. @@ -308,9 +304,9 @@ class NpxStdioTransport(StdioTransport): def __init__( self, package: str, - args: Optional[List[str]] = None, - project_directory: Optional[str] = None, - env_vars: Optional[Dict[str, str]] = None, + args: list[str] | None = None, + project_directory: str | None = None, + env_vars: dict[str, str] | None = None, use_package_lock: bool = True, ): """ @@ -394,7 +390,7 @@ def infer_transport( return FastMCPTransport(mcp=transport) # the transport is a path to a script - elif isinstance(transport, (Path, str)) and Path(transport).exists(): + elif isinstance(transport, Path | str) and Path(transport).exists(): if str(transport).endswith(".py"): return PythonStdioTransport(script_path=transport) elif str(transport).endswith(".js"): @@ -403,11 +399,11 @@ def infer_transport( raise ValueError(f"Unsupported script type: {transport}") # the transport is an http(s) URL - elif isinstance(transport, (AnyUrl, str)) and str(transport).startswith("http"): + elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"): return SSETransport(url=transport) # the transport is a websocket URL - elif isinstance(transport, (AnyUrl, str)) and str(transport).startswith("ws"): + elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"): return WSTransport(url=transport) # the transport is an unknown type diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 49d55dff1..a37c6dba4 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -4,7 +4,8 @@ import enum import json import re from dataclasses import dataclass -from typing import Any, Literal, Pattern +from re import Pattern +from typing import Any, Literal import httpx from pydantic.networks import AnyUrl diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index e2c26efcc..53c95d3fe 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -99,7 +99,7 @@ class FastMCP(Generic[LifespanResultT]): self.dependencies = self.settings.dependencies # Setup for mounted apps - self._mounted_apps: dict[str, "FastMCP"] = {} + self._mounted_apps: dict[str, FastMCP] = {} # Set up MCP protocol handlers self._setup_handlers() @@ -640,7 +640,7 @@ def _convert_to_content( other_content = [] for item in result: - if isinstance(item, (TextContent, ImageContent, EmbeddedResource, Image)): + if isinstance(item, TextContent | ImageContent | EmbeddedResource | Image): mcp_types.append(_convert_to_content(item)[0]) else: other_content.append(item) diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 48e2fdc9a..cd8c882b8 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -62,7 +62,7 @@ class ToolManager: return await tool.run(arguments, context=context) def import_tools( - self, tool_manager: "ToolManager", prefix: str | None = None + self, tool_manager: ToolManager, prefix: str | None = None ) -> None: """ Import all tools from another ToolManager with prefixed names. diff --git a/src/fastmcp/utilities/func_metadata.py b/src/fastmcp/utilities/func_metadata.py index 5673b5a26..e9d47b843 100644 --- a/src/fastmcp/utilities/func_metadata.py +++ b/src/fastmcp/utilities/func_metadata.py @@ -27,7 +27,7 @@ class ArgModelBase(BaseModel): That is, sub-models etc are not dumped - they are kept as pydantic models. """ kwargs: dict[str, Any] = {} - for field_name in self.model_fields.keys(): + for field_name in self.__class__.model_fields.keys(): kwargs[field_name] = getattr(self, field_name) return kwargs diff --git a/src/fastmcp/utilities/openapi.py b/src/fastmcp/utilities/openapi.py index 68abf0126..4427951b2 100644 --- a/src/fastmcp/utilities/openapi.py +++ b/src/fastmcp/utilities/openapi.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any, Literal, Union, cast +from typing import Any, Literal, cast # Using the recommended library: openapi-pydantic from openapi_pydantic import ( @@ -92,7 +92,7 @@ __all__ = [ def _resolve_ref( - item: Union[Reference, Schema, Parameter, RequestBody, Any], openapi: OpenAPI + item: Reference | Schema | Parameter | RequestBody | Any, openapi: OpenAPI ) -> Any: """Resolves a potential Reference object to its target definition (no changes needed here).""" if isinstance(item, Reference): @@ -110,7 +110,7 @@ def _resolve_ref( elif isinstance(target, BaseModel): # Use model_extra for fields not explicitly defined (like components types) # Check class fields first, then model_extra - if part in target.model_fields: # Access class attribute here + if part in target.__class__.model_fields: target = getattr(target, part, None) elif target.model_extra and part in target.model_extra: target = target.model_extra[part] @@ -141,7 +141,7 @@ def _resolve_ref( def _extract_schema_as_dict( - schema_obj: Union[Schema, Reference], openapi: OpenAPI + schema_obj: Schema | Reference, openapi: OpenAPI ) -> JsonSchema: """Resolves a schema/reference and returns it as a dictionary.""" resolved_schema = _resolve_ref(schema_obj, openapi) @@ -177,8 +177,8 @@ def _convert_to_parameter_location(param_in: str) -> ParameterLocation: def _extract_parameters( - operation_params: list[Union[Parameter, Reference]] | None, - path_item_params: list[Union[Parameter, Reference]] | None, + operation_params: list[Parameter | Reference] | None, + path_item_params: list[Parameter | Reference] | None, openapi: OpenAPI, ) -> list[ParameterInfo]: """Extracts and resolves parameters using corrected attribute names.""" diff --git a/tests/utilities/openapi/test_openapi.py b/tests/utilities/openapi/test_openapi.py index 0505b5e12..410fef210 100644 --- a/tests/utilities/openapi/test_openapi.py +++ b/tests/utilities/openapi/test_openapi.py @@ -1,6 +1,6 @@ """Tests for the OpenAPI parsing utilities.""" -from typing import Any, Dict +from typing import Any import pytest from fastapi import Body, FastAPI, Path, Query @@ -12,7 +12,7 @@ from fastmcp.utilities.openapi import parse_openapi_to_http_routes @pytest.fixture -def petstore_schema() -> Dict[str, Any]: +def petstore_schema() -> dict[str, Any]: """Fixture that returns a simple Pet Store API schema.""" return { "openapi": "3.1.0", @@ -109,7 +109,7 @@ def parsed_petstore_routes(petstore_schema): @pytest.fixture -def bookstore_schema() -> Dict[str, Any]: +def bookstore_schema() -> dict[str, Any]: """Fixture that returns a Book Store API schema with different parameter types.""" return { "openapi": "3.1.0", @@ -292,7 +292,7 @@ def fastapi_app() -> FastAPI: @pytest.fixture -def fastapi_openapi_schema(fastapi_app) -> Dict[str, Any]: +def fastapi_openapi_schema(fastapi_app) -> dict[str, Any]: """Fixture that returns the OpenAPI schema of the FastAPI app.""" return fastapi_app.openapi() diff --git a/tests/utilities/openapi/test_openapi_advanced.py b/tests/utilities/openapi/test_openapi_advanced.py index bacb5fbbd..6b7ec8af3 100644 --- a/tests/utilities/openapi/test_openapi_advanced.py +++ b/tests/utilities/openapi/test_openapi_advanced.py @@ -1,6 +1,6 @@ """Tests for advanced features of the OpenAPI utilities.""" -from typing import Any, Dict +from typing import Any import pytest @@ -8,7 +8,7 @@ from fastmcp.utilities.openapi import parse_openapi_to_http_routes @pytest.fixture -def complex_schema() -> Dict[str, Any]: +def complex_schema() -> dict[str, Any]: """Fixture that returns a complex OpenAPI schema with nested references.""" return { "openapi": "3.1.0", @@ -167,7 +167,7 @@ def complex_route_map(parsed_complex_routes): @pytest.fixture -def schema_with_invalid_reference() -> Dict[str, Any]: +def schema_with_invalid_reference() -> dict[str, Any]: """Fixture that returns a schema with an invalid reference.""" return { "openapi": "3.1.0", @@ -191,7 +191,7 @@ def schema_with_invalid_reference() -> Dict[str, Any]: @pytest.fixture -def schema_with_content_params() -> Dict[str, Any]: +def schema_with_content_params() -> dict[str, Any]: """Fixture that returns a schema with content-based parameters (complex parameters).""" return { "openapi": "3.1.0", @@ -236,7 +236,7 @@ def parsed_content_param_routes(schema_with_content_params): @pytest.fixture -def schema_all_http_methods() -> Dict[str, Any]: +def schema_all_http_methods() -> dict[str, Any]: """Fixture that returns a schema with all HTTP methods.""" return { "openapi": "3.1.0", diff --git a/tests/utilities/openapi/test_openapi_fastapi.py b/tests/utilities/openapi/test_openapi_fastapi.py index e0ed67d71..b7da748cc 100644 --- a/tests/utilities/openapi/test_openapi_fastapi.py +++ b/tests/utilities/openapi/test_openapi_fastapi.py @@ -1,6 +1,6 @@ """Tests for FastAPI integration with the OpenAPI utilities.""" -from typing import Any, Dict +from typing import Any import pytest from fastapi import FastAPI @@ -12,7 +12,6 @@ from fastmcp.utilities.openapi import parse_openapi_to_http_routes def fastapi_server() -> FastAPI: """Fixture that returns a FastAPI app for live OpenAPI schema testing.""" from enum import Enum - from typing import List from fastapi import Body, Depends, Header, HTTPException, Path, Query from pydantic import BaseModel, Field @@ -145,7 +144,7 @@ def fastapi_server() -> FastAPI: ) async def update_item_tags( item_id: int = Path(..., description="The ID of the item"), - tags: List[str] = Body(..., description="Updated tags"), + tags: list[str] = Body(..., description="Updated tags"), ): """Update just the tags of an item.""" return {"item_id": item_id, "tags": tags} @@ -229,7 +228,7 @@ def fastapi_server() -> FastAPI: @pytest.fixture -def fastapi_openapi_schema(fastapi_server) -> Dict[str, Any]: +def fastapi_openapi_schema(fastapi_server) -> dict[str, Any]: """Fixture that returns the OpenAPI schema from a live FastAPI server.""" return fastapi_server.openapi() diff --git a/uv.lock b/uv.lock index a3b445223..72cdc17b9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" [[package]] @@ -127,7 +128,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -230,7 +231,7 @@ name = "fancycompleter" version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline", marker = "platform_system == 'Windows'" }, + { name = "pyreadline", marker = "sys_platform == 'win32'" }, { name = "pyrepl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/95/649d135442d8ecf8af5c7e235550c628056423c96c4bc6787348bdae9248/fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272", size = 10866 } @@ -254,7 +255,6 @@ wheels = [ [[package]] name = "fastmcp" -version = "0.4.2.dev40+g8720a78.d20250410" source = { editable = "." } dependencies = [ { name = "dotenv" },