mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Compare commits
3 commits
main
...
maint/exam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d060e93eec |
||
|
|
824603517d |
||
|
|
3c484aa4a9 |
6 changed files with 162 additions and 17 deletions
|
|
@ -23,7 +23,7 @@ import base64
|
|||
import io
|
||||
|
||||
import qrcode # type: ignore[import-untyped]
|
||||
from mcp import types
|
||||
from mcp_types import ImageContent
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps import AppConfig, ResourceCSP
|
||||
|
|
@ -153,7 +153,7 @@ def generate_qr(
|
|||
img.save(buffer, format="PNG")
|
||||
b64 = base64.b64encode(buffer.getvalue()).decode()
|
||||
return ToolResult(
|
||||
content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||
content=[ImageContent(type="image", data=b64, mime_type="image/png")]
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from typing import Any
|
|||
|
||||
import yaml
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.tools.tool import ToolResult
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.tools import ToolResult
|
||||
|
||||
|
||||
def with_serializer(serializer: Callable[[Any], str]):
|
||||
|
|
@ -55,18 +55,18 @@ def get_json_data() -> dict:
|
|||
|
||||
|
||||
async def example_usage():
|
||||
# YAML serialized tool
|
||||
yaml_result = await server._call_tool_mcp("get_example_data", {})
|
||||
print("YAML Tool Result:")
|
||||
print(yaml_result)
|
||||
print()
|
||||
async with Client(server) as client:
|
||||
# YAML serialized tool
|
||||
yaml_result = await client.call_tool("get_example_data", {})
|
||||
print("YAML Tool Result:")
|
||||
print(yaml_result.content[0].text)
|
||||
print()
|
||||
|
||||
# Default JSON serialized tool
|
||||
json_result = await server._call_tool_mcp("get_json_data", {})
|
||||
print("JSON Tool Result:")
|
||||
print(json_result)
|
||||
# Default JSON serialized tool
|
||||
json_result = await client.call_tool("get_json_data", {})
|
||||
print("JSON Tool Result:")
|
||||
print(json_result.content[0].text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(example_usage())
|
||||
server.run()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from rich import print
|
|||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.server.providers import Provider
|
||||
from fastmcp.tools.tool import Tool, ToolResult
|
||||
from fastmcp.tools import Tool, ToolResult
|
||||
|
||||
DB_PATH = Path(__file__).parent / "tools.db"
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ async def main():
|
|||
print("=== Resource Templates ===")
|
||||
templates = await client.list_resource_templates()
|
||||
for t in templates:
|
||||
print(f" {t.uriTemplate}")
|
||||
print(f" {t.uri_template}")
|
||||
print()
|
||||
|
||||
# Read a skill's main file
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import time
|
|||
from dataclasses import dataclass
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.tools.tool import ToolResult
|
||||
from fastmcp.tools import ToolResult
|
||||
|
||||
mcp = FastMCP("Echo Server")
|
||||
|
||||
|
|
|
|||
145
tests/test_examples_importable.py
Normal file
145
tests/test_examples_importable.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""Import-resolution gate for the ``examples/`` directory.
|
||||
|
||||
The example scripts sit outside the ty and pytest import gates, so stale
|
||||
SDK idioms (renamed modules, moved symbols, v1 import paths) shipped there
|
||||
repeatedly without CI noticing. This test closes that gap: it discovers every
|
||||
``.py`` under ``examples/`` and asserts, WITHOUT executing any of them, that
|
||||
|
||||
1. the file parses as valid Python (AST), and
|
||||
2. every top-level ``fastmcp`` / ``mcp`` / ``mcp_types`` import resolves
|
||||
against the installed package — both the module path and the imported
|
||||
names. This catches the recurring ``from mcp.types import X`` regression
|
||||
(the module is now ``mcp_types``) as well as renamed or moved symbols.
|
||||
|
||||
Execution is deliberately avoided: examples spin up servers, hit external
|
||||
services, and pull heavy optional dependencies. Static resolution catches the
|
||||
class of breakage we actually keep reintroducing (renamed imports) cheaply.
|
||||
|
||||
Standalone example sub-projects with their own ``uv.lock`` (e.g.
|
||||
``examples/testing_demo`` targets v1 on purpose) are excluded — their
|
||||
dependency graph is independently resolved, so their imports are validated
|
||||
against a different package than the one installed here. A bare
|
||||
``pyproject.toml`` without its own lock (e.g. ``examples/apps/qr_server``)
|
||||
still resolves against this tree's install and is checked like any other
|
||||
example — it is not, on its own, evidence of an incompatible dependency.
|
||||
|
||||
Run:
|
||||
uv run pytest tests/test_examples_importable.py -v -s
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
EXAMPLES_DIR = Path(__file__).parent.parent / "examples"
|
||||
|
||||
# Snapshot baseline — ratchet DOWN as examples are fixed, never up.
|
||||
MAX_IMPORT_FAILURES = 0
|
||||
|
||||
_CHECKED_ROOTS = ("fastmcp", "mcp", "mcp_types")
|
||||
|
||||
|
||||
def _standalone_dirs() -> set[Path]:
|
||||
"""Directories that are self-contained example sub-projects.
|
||||
|
||||
A ``uv.lock`` under ``examples/`` marks a genuinely independent dependency
|
||||
graph (``examples/testing_demo`` locks and targets fastmcp v1 on purpose),
|
||||
so its imports must not be validated against the package installed for
|
||||
the main test suite. A ``pyproject.toml`` alone is not sufficient — most
|
||||
example sub-projects (``examples/apps/qr_server``, ``examples/smart_home``,
|
||||
``examples/atproto_mcp``) have one purely to declare extra dependencies
|
||||
(qrcode, phue2, atproto) but share this tree's fastmcp/mcp install, so
|
||||
their fastmcp/mcp imports are still checked. Everything below a directory
|
||||
with its own lock is excluded.
|
||||
"""
|
||||
return {lockfile.parent for lockfile in EXAMPLES_DIR.rglob("uv.lock")}
|
||||
|
||||
|
||||
def _find_example_files() -> list[Path]:
|
||||
standalone = _standalone_dirs()
|
||||
|
||||
def is_standalone(path: Path) -> bool:
|
||||
return any(root in path.parents for root in standalone)
|
||||
|
||||
return sorted(p for p in EXAMPLES_DIR.rglob("*.py") if not is_standalone(p))
|
||||
|
||||
|
||||
def _check_imports(path: Path) -> list[str]:
|
||||
"""Return descriptions of unresolved fastmcp/mcp_types imports."""
|
||||
try:
|
||||
tree = ast.parse(path.read_text("utf-8"))
|
||||
except SyntaxError as e:
|
||||
rel = path.relative_to(EXAMPLES_DIR.parent)
|
||||
return [f"{rel}:{e.lineno}: syntax error: {e.msg}"]
|
||||
|
||||
errors: list[str] = []
|
||||
rel = path.relative_to(EXAMPLES_DIR.parent)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name.split(".")[0] in _CHECKED_ROOTS:
|
||||
if _import_module(alias.name) is None:
|
||||
errors.append(
|
||||
f"{rel}:{node.lineno}: cannot import '{alias.name}'"
|
||||
)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level: # relative import — not a fastmcp/mcp_types path
|
||||
continue
|
||||
if node.module and node.module.split(".")[0] in _CHECKED_ROOTS:
|
||||
mod = _import_module(node.module)
|
||||
if mod is None:
|
||||
errors.append(
|
||||
f"{rel}:{node.lineno}: cannot import module '{node.module}'"
|
||||
)
|
||||
continue
|
||||
for alias in node.names:
|
||||
name = alias.name
|
||||
if name == "*":
|
||||
continue
|
||||
# ``from pkg import sub`` where ``sub`` is itself a
|
||||
# submodule resolves even though ``pkg`` has no such
|
||||
# attribute until the submodule is imported.
|
||||
if hasattr(mod, name) or _import_module(f"{node.module}.{name}"):
|
||||
continue
|
||||
errors.append(
|
||||
f"{rel}:{node.lineno}: '{node.module}' has no '{name}'"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _import_module(name: str):
|
||||
"""Import ``name``, returning the module or None if it cannot be imported."""
|
||||
try:
|
||||
return importlib.import_module(name)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def test_examples_imports_resolve():
|
||||
"""Example scripts must not regress in fastmcp/mcp_types import resolution.
|
||||
|
||||
Parses every ``.py`` under ``examples/`` (excluding standalone sub-projects
|
||||
that pin their own fastmcp/mcp) and verifies its top-level fastmcp and
|
||||
mcp_types imports resolve against the installed package. Nothing is
|
||||
executed.
|
||||
"""
|
||||
files = _find_example_files()
|
||||
assert files, "no example files discovered — check EXAMPLES_DIR"
|
||||
|
||||
import_failures: list[str] = []
|
||||
for path in files:
|
||||
import_failures.extend(_check_imports(path))
|
||||
|
||||
print(f"\nExample files checked: {len(files)}")
|
||||
print(f"Import failures: {len(import_failures)}")
|
||||
if import_failures:
|
||||
print("\nUnresolved imports:")
|
||||
for failure in import_failures:
|
||||
print(f" {failure}")
|
||||
|
||||
assert len(import_failures) <= MAX_IMPORT_FAILURES, (
|
||||
f"Import failures regressed: {len(import_failures)} > {MAX_IMPORT_FAILURES}"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue