mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Lazy-load heavy imports to reduce import time
Defer auth providers (JWTVerifier, OAuthProxy, OIDCProxy) and Client to avoid eagerly importing authlib, cryptography, key_value.aio, and beartype on every `from fastmcp import FastMCP`.
This commit is contained in:
parent
270783e614
commit
d9541c9c21
5 changed files with 275 additions and 9 deletions
212
scripts/benchmark_imports.py
Normal file
212
scripts/benchmark_imports.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Benchmark import times for fastmcp and its dependency chain.
|
||||||
|
|
||||||
|
Each measurement runs in a fresh subprocess so there's no shared module cache.
|
||||||
|
Incremental costs are measured by pre-importing dependencies, so we can see
|
||||||
|
what each module truly adds.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
uv run python scripts/benchmark_imports.py
|
||||||
|
uv run python scripts/benchmark_imports.py --runs 10
|
||||||
|
uv run python scripts/benchmark_imports.py --json
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BenchmarkCase:
|
||||||
|
label: str
|
||||||
|
stmt: str
|
||||||
|
prereqs: str = ""
|
||||||
|
group: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
CASES = [
|
||||||
|
# --- Floor ---
|
||||||
|
BenchmarkCase("pydantic", "import pydantic", group="floor"),
|
||||||
|
BenchmarkCase("mcp", "import mcp", group="floor"),
|
||||||
|
BenchmarkCase(
|
||||||
|
"mcp (server only)", "import mcp.server.lowlevel.server", group="floor"
|
||||||
|
),
|
||||||
|
# --- Auth stack (incremental over mcp) ---
|
||||||
|
BenchmarkCase(
|
||||||
|
"authlib.jose", "import authlib.jose", prereqs="import mcp", group="auth"
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"cryptography.fernet",
|
||||||
|
"from cryptography.fernet import Fernet",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="auth",
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"authlib.integrations.httpx_client",
|
||||||
|
"from authlib.integrations.httpx_client import AsyncOAuth2Client",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="auth",
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"key_value.aio", "import key_value.aio", prereqs="import mcp", group="auth"
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"key_value.aio.stores.filetree",
|
||||||
|
"from key_value.aio.stores.filetree import FileTreeStore",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="auth",
|
||||||
|
),
|
||||||
|
BenchmarkCase("beartype", "import beartype", prereqs="import mcp", group="auth"),
|
||||||
|
# --- Docket stack (incremental over mcp) ---
|
||||||
|
BenchmarkCase("redis", "import redis", prereqs="import mcp", group="docket"),
|
||||||
|
BenchmarkCase(
|
||||||
|
"opentelemetry.sdk.metrics",
|
||||||
|
"import opentelemetry.sdk.metrics",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="docket",
|
||||||
|
),
|
||||||
|
BenchmarkCase("docket", "import docket", prereqs="import mcp", group="docket"),
|
||||||
|
BenchmarkCase("croniter", "import croniter", prereqs="import mcp", group="docket"),
|
||||||
|
# --- Other deps (incremental over mcp) ---
|
||||||
|
BenchmarkCase("httpx", "import httpx", prereqs="import mcp", group="other"),
|
||||||
|
BenchmarkCase(
|
||||||
|
"starlette",
|
||||||
|
"from starlette.applications import Starlette",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="other",
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"pydantic_settings",
|
||||||
|
"import pydantic_settings",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="other",
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"rich.console", "import rich.console", prereqs="import mcp", group="other"
|
||||||
|
),
|
||||||
|
BenchmarkCase("jsonref", "import jsonref", prereqs="import mcp", group="other"),
|
||||||
|
BenchmarkCase("requests", "import requests", prereqs="import mcp", group="other"),
|
||||||
|
# --- FastMCP (total and incremental) ---
|
||||||
|
BenchmarkCase("fastmcp (total)", "from fastmcp import FastMCP", group="fastmcp"),
|
||||||
|
BenchmarkCase(
|
||||||
|
"fastmcp (over mcp)",
|
||||||
|
"from fastmcp import FastMCP",
|
||||||
|
prereqs="import mcp",
|
||||||
|
group="fastmcp",
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"fastmcp (over mcp+docket)",
|
||||||
|
"from fastmcp import FastMCP",
|
||||||
|
prereqs="import mcp; import docket",
|
||||||
|
group="fastmcp",
|
||||||
|
),
|
||||||
|
BenchmarkCase(
|
||||||
|
"fastmcp (over mcp+docket+auth deps)",
|
||||||
|
"from fastmcp import FastMCP",
|
||||||
|
prereqs=(
|
||||||
|
"import mcp; import docket; import authlib.jose;"
|
||||||
|
" from cryptography.fernet import Fernet;"
|
||||||
|
" import key_value.aio"
|
||||||
|
),
|
||||||
|
group="fastmcp",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def measure_once(stmt: str, prereqs: str) -> float | None:
|
||||||
|
pre = prereqs + "; " if prereqs else ""
|
||||||
|
code = (
|
||||||
|
f"{pre}"
|
||||||
|
"import time as _t; _s=_t.perf_counter(); "
|
||||||
|
f"{stmt}; "
|
||||||
|
"print(f'{(_t.perf_counter()-_s)*1000:.2f}')"
|
||||||
|
)
|
||||||
|
r = subprocess.run(
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if r.returncode == 0 and r.stdout.strip():
|
||||||
|
return float(r.stdout.strip())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def measure(case: BenchmarkCase, runs: int) -> dict[str, float | str | None]:
|
||||||
|
times: list[float] = []
|
||||||
|
for _ in range(runs):
|
||||||
|
t = measure_once(case.stmt, case.prereqs)
|
||||||
|
if t is not None:
|
||||||
|
times.append(t)
|
||||||
|
|
||||||
|
if not times:
|
||||||
|
return {"label": case.label, "group": case.group, "median_ms": None}
|
||||||
|
|
||||||
|
times.sort()
|
||||||
|
median = times[len(times) // 2]
|
||||||
|
return {
|
||||||
|
"label": case.label,
|
||||||
|
"group": case.group,
|
||||||
|
"median_ms": round(median, 1),
|
||||||
|
"min_ms": round(times[0], 1),
|
||||||
|
"max_ms": round(times[-1], 1),
|
||||||
|
"runs": len(times),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_table(results: list[dict[str, float | str | None]]) -> None:
|
||||||
|
current_group = None
|
||||||
|
print(f"\n{'Module':<45} {'Median':>8} {'Min':>8} {'Max':>8}")
|
||||||
|
print("-" * 71)
|
||||||
|
for r in results:
|
||||||
|
if r["group"] != current_group:
|
||||||
|
current_group = r["group"]
|
||||||
|
group_labels = {
|
||||||
|
"floor": "--- Unavoidable floor ---",
|
||||||
|
"auth": "--- Auth stack (incremental over mcp) ---",
|
||||||
|
"docket": "--- Docket stack (incremental over mcp) ---",
|
||||||
|
"other": "--- Other deps (incremental over mcp) ---",
|
||||||
|
"fastmcp": "--- FastMCP totals ---",
|
||||||
|
}
|
||||||
|
print(f"\n{group_labels.get(current_group, current_group)}")
|
||||||
|
if r["median_ms"] is not None:
|
||||||
|
print(
|
||||||
|
f" {r['label']:<43} {r['median_ms']:>7.1f}ms"
|
||||||
|
f" {r['min_ms']:>7.1f}ms {r['max_ms']:>7.1f}ms"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f" {r['label']:<43} error")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Benchmark fastmcp import times")
|
||||||
|
parser.add_argument(
|
||||||
|
"--runs", type=int, default=5, help="Number of runs per measurement (default 5)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--json", action="store_true", help="Output results as JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"Benchmarking import times ({args.runs} runs each)...")
|
||||||
|
print(f"Python: {sys.version.split()[0]}")
|
||||||
|
print(f"Executable: {sys.executable}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for case in CASES:
|
||||||
|
r = measure(case, args.runs)
|
||||||
|
results.append(r)
|
||||||
|
if not args.json:
|
||||||
|
ms = f"{r['median_ms']:.1f}ms" if r["median_ms"] is not None else "error"
|
||||||
|
print(f" {case.label}: {ms}")
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(results, indent=2))
|
||||||
|
else:
|
||||||
|
print_table(results)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
"""FastMCP - An ergonomic MCP interface."""
|
"""FastMCP - An ergonomic MCP interface."""
|
||||||
|
|
||||||
|
import importlib
|
||||||
import warnings
|
import warnings
|
||||||
from importlib.metadata import version as _version
|
from importlib.metadata import version as _version
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastmcp.settings import Settings
|
from fastmcp.settings import Settings
|
||||||
from fastmcp.utilities.logging import configure_logging as _configure_logging
|
from fastmcp.utilities.logging import configure_logging as _configure_logging
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from fastmcp.client import Client as Client
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
if settings.log_enabled:
|
if settings.log_enabled:
|
||||||
_configure_logging(
|
_configure_logging(
|
||||||
|
|
@ -16,9 +22,6 @@ from fastmcp.server.server import FastMCP
|
||||||
from fastmcp.server.context import Context
|
from fastmcp.server.context import Context
|
||||||
import fastmcp.server
|
import fastmcp.server
|
||||||
|
|
||||||
from fastmcp.client import Client
|
|
||||||
from . import client
|
|
||||||
|
|
||||||
__version__ = _version("fastmcp")
|
__version__ = _version("fastmcp")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,6 +30,21 @@ if settings.deprecation_warnings:
|
||||||
warnings.simplefilter("default", DeprecationWarning)
|
warnings.simplefilter("default", DeprecationWarning)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Lazy imports for performance (see #3292) ---
|
||||||
|
# Client and the client submodule are deferred so that server-only users
|
||||||
|
# don't pay for the client import chain. Do not convert back to top-level.
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> object:
|
||||||
|
if name == "Client":
|
||||||
|
from fastmcp.client import Client
|
||||||
|
|
||||||
|
return Client
|
||||||
|
if name == "client":
|
||||||
|
return importlib.import_module("fastmcp.client")
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Client",
|
"Client",
|
||||||
"Context",
|
"Context",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .server import FastMCP, create_proxy
|
from .server import FastMCP, create_proxy
|
||||||
from . import dependencies
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Context", "FastMCP", "create_proxy"]
|
__all__ = ["Context", "FastMCP", "create_proxy"]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .auth import (
|
from .auth import (
|
||||||
OAuthProvider,
|
OAuthProvider,
|
||||||
TokenVerifier,
|
TokenVerifier,
|
||||||
|
|
@ -12,10 +14,44 @@ from .authorization import (
|
||||||
restrict_tag,
|
restrict_tag,
|
||||||
run_auth_checks,
|
run_auth_checks,
|
||||||
)
|
)
|
||||||
from .providers.debug import DebugTokenVerifier
|
|
||||||
from .providers.jwt import JWTVerifier, StaticTokenVerifier
|
if TYPE_CHECKING:
|
||||||
from .oauth_proxy import OAuthProxy
|
from .oauth_proxy import OAuthProxy as OAuthProxy
|
||||||
from .oidc_proxy import OIDCProxy
|
from .oidc_proxy import OIDCProxy as OIDCProxy
|
||||||
|
from .providers.debug import DebugTokenVerifier as DebugTokenVerifier
|
||||||
|
from .providers.jwt import JWTVerifier as JWTVerifier
|
||||||
|
from .providers.jwt import StaticTokenVerifier as StaticTokenVerifier
|
||||||
|
|
||||||
|
|
||||||
|
# --- Lazy imports for performance (see #3292) ---
|
||||||
|
# These providers pull in heavy deps (authlib, cryptography, key_value.aio,
|
||||||
|
# beartype) that most users never need. Keeping them behind __getattr__
|
||||||
|
# avoids ~150ms+ of import overhead for the common server-only case.
|
||||||
|
# Do not convert these back to top-level imports.
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> object:
|
||||||
|
if name == "DebugTokenVerifier":
|
||||||
|
from .providers.debug import DebugTokenVerifier
|
||||||
|
|
||||||
|
return DebugTokenVerifier
|
||||||
|
if name == "JWTVerifier":
|
||||||
|
from .providers.jwt import JWTVerifier
|
||||||
|
|
||||||
|
return JWTVerifier
|
||||||
|
if name == "StaticTokenVerifier":
|
||||||
|
from .providers.jwt import StaticTokenVerifier
|
||||||
|
|
||||||
|
return StaticTokenVerifier
|
||||||
|
if name == "OAuthProxy":
|
||||||
|
from .oauth_proxy import OAuthProxy
|
||||||
|
|
||||||
|
return OAuthProxy
|
||||||
|
if name == "OIDCProxy":
|
||||||
|
from .oidc_proxy import OIDCProxy
|
||||||
|
|
||||||
|
return OIDCProxy
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ from fastmcp.server.apps import (
|
||||||
resolve_ui_mime_type,
|
resolve_ui_mime_type,
|
||||||
)
|
)
|
||||||
from fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks
|
from fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks
|
||||||
from fastmcp.server.dependencies import get_access_token
|
|
||||||
from fastmcp.server.lifespan import Lifespan
|
from fastmcp.server.lifespan import Lifespan
|
||||||
from fastmcp.server.low_level import LowLevelServer
|
from fastmcp.server.low_level import LowLevelServer
|
||||||
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
||||||
|
|
@ -162,6 +161,8 @@ def _get_auth_context() -> tuple[bool, Any]:
|
||||||
is_stdio = _current_transport.get() == "stdio"
|
is_stdio = _current_transport.get() == "stdio"
|
||||||
if is_stdio:
|
if is_stdio:
|
||||||
return (True, None)
|
return (True, None)
|
||||||
|
from fastmcp.server.dependencies import get_access_token
|
||||||
|
|
||||||
return (False, get_access_token())
|
return (False, get_access_token())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue